Skip to content

Commit

Permalink
feat: record application state in undo/redo history
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvxd committed Oct 17, 2023
1 parent 1b6c39c commit 0f2d7c5
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 33 deletions.
2 changes: 1 addition & 1 deletion packages/core/components/LayerTree/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const LayerTree = ({
}: {
data: Data;
zoneContent: Data["content"];
itemSelector: ItemSelector | null;
itemSelector?: ItemSelector | null;
setItemSelector: (item: ItemSelector | null) => void;
zone?: string;
label?: string;
Expand Down
27 changes: 20 additions & 7 deletions packages/core/components/Puck/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,21 +88,31 @@ export function Puck({
}) {
const [reducer] = useState(() => createReducer({ config }));

const initialAppData = { data: initialData, state: {} };
const initialAppData = {
data: initialData,
state: { leftSideBarVisible: true, itemSelector: null },
};

const [appData, dispatch] = useReducer<StateReducer>(
reducer,
flushZones(initialAppData)
);

const { data } = appData;
const { data, state } = appData;

const { canForward, canRewind, rewind, forward } = usePuckHistory({
appData,
dispatch,
});

const [itemSelector, setItemSelector] = useState<ItemSelector | null>(null);
const { itemSelector, leftSideBarVisible } = state;

const setItemSelector = useCallback(
(newItemSelector: ItemSelector | null) => {
dispatch({ type: "setState", state: { itemSelector: newItemSelector } });
},
[]
);

const selectedItem = itemSelector ? getItem(itemSelector, data) : null;

Expand Down Expand Up @@ -164,8 +174,6 @@ export function Puck({

const { onDragStartOrUpdate, placeholderStyle } = usePlaceholderStyle();

const [leftSidebarVisible, setLeftSidebarVisible] = useState(true);

const [draggedItem, setDraggedItem] = useState<
DragStart & Partial<DragUpdate>
>();
Expand Down Expand Up @@ -266,7 +274,7 @@ export function Puck({
gridTemplateAreas:
'"header header header" "left editor right"',
gridTemplateColumns: `${
leftSidebarVisible ? "288px" : "0px"
leftSideBarVisible ? "288px" : "0px"
} auto 288px`,
gridTemplateRows: "min-content auto",
height: "100vh",
Expand Down Expand Up @@ -318,7 +326,12 @@ export function Puck({
>
<IconButton
onClick={() =>
setLeftSidebarVisible(!leftSidebarVisible)
dispatch({
type: "setState",
state: {
leftSideBarVisible: !leftSideBarVisible,
},
})
}
title="Toggle left sidebar"
>
Expand Down
4 changes: 2 additions & 2 deletions packages/core/lib/use-puck-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function usePuckHistory({
applyChange(target, true, change);
return target;
}, target);
dispatch({ type: "setData", data: target.data });
dispatch({ type: "set", appData: target });
},
rewind: () => {
const target = structuredClone(appData);
Expand All @@ -51,7 +51,7 @@ export function usePuckHistory({
return target;
}, target);

dispatch({ type: "setData", data: target.data });
dispatch({ type: "set", appData: target });
},
});
}, DEBOUNCE_TIME);
Expand Down
40 changes: 21 additions & 19 deletions packages/core/reducer/__tests__/data.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
UnregisterZoneAction,
createReducer,
} from "../../reducer";
import { AppData, Config, Data } from "../../types/Config";
import { AppData, AppState, Config, Data } from "../../types/Config";
import { rootDroppableId } from "../../lib/root-droppable-id";

import { generateId } from "../../lib/generate-id";
Expand All @@ -26,6 +26,8 @@ type Props = {
};
const defaultData: Data = { root: { title: "" }, content: [], zones: {} };

const defaultState: AppState = { leftSideBarVisible: true };

describe("Data reducer", () => {
const config: Config<Props> = {
components: {
Expand All @@ -40,7 +42,7 @@ describe("Data reducer", () => {

describe("insert action", () => {
it("should insert into rootDroppableId", () => {
const state: AppData = { state: {}, data: { ...defaultData } };
const state: AppData = { state: defaultState, data: { ...defaultData } };

const action: InsertAction = {
type: "insert",
Expand All @@ -55,7 +57,7 @@ describe("Data reducer", () => {
});

it("should insert into a different zone", () => {
const state: AppData = { state: {}, data: { ...defaultData } };
const state: AppData = { state: defaultState, data: { ...defaultData } };
const action: InsertAction = {
type: "insert",
componentType: "Comp",
Expand All @@ -76,7 +78,7 @@ describe("Data reducer", () => {
describe("reorder action", () => {
it("should reorder within rootDroppableId", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
content: [
Expand All @@ -99,7 +101,7 @@ describe("Data reducer", () => {

it("should reorder within a different zone", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
zones: {
Expand All @@ -126,7 +128,7 @@ describe("Data reducer", () => {
describe("duplicate action", () => {
it("should duplicate in content", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
content: [
Expand All @@ -151,7 +153,7 @@ describe("Data reducer", () => {

it("should duplicate in a different zone", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
zones: {
Expand Down Expand Up @@ -184,7 +186,7 @@ describe("Data reducer", () => {
mockedGenerateId.mockImplementation(() => `mockId-${counter++}`);

const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
zones: {
Expand Down Expand Up @@ -262,7 +264,7 @@ describe("Data reducer", () => {
describe("move action", () => {
it("should move from rootDroppableId to another zone", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
content: [{ type: "Comp", props: { id: "1" } }],
Expand All @@ -284,7 +286,7 @@ describe("Data reducer", () => {

it("should move from a zone to rootDroppableId", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
content: [{ type: "Comp", props: { id: "1" } }],
Expand All @@ -306,7 +308,7 @@ describe("Data reducer", () => {

it("should move between two zones", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
content: [],
Expand Down Expand Up @@ -335,7 +337,7 @@ describe("Data reducer", () => {

it("should replace in content", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
content: [{ type: "Comp", props: { id: "1" } }],
Expand All @@ -355,7 +357,7 @@ describe("Data reducer", () => {

it("should replace in a zone", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
zones: { zone1: [{ type: "Comp", props: { id: "1" } }] },
Expand All @@ -377,7 +379,7 @@ describe("Data reducer", () => {
describe("remove action", () => {
it("should remove from content", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
content: [{ type: "Comp", props: { id: "1" } }],
Expand All @@ -395,7 +397,7 @@ describe("Data reducer", () => {

it("should remove from a zone", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
zones: { zone1: [{ type: "Comp", props: { id: "1" } }] },
Expand All @@ -417,7 +419,7 @@ describe("Data reducer", () => {
mockedGenerateId.mockImplementation(() => `mockId-${counter++}`);

const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
zones: {
Expand Down Expand Up @@ -462,7 +464,7 @@ describe("Data reducer", () => {
describe("unregisterZone action", () => {
it("should unregister a zone", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
zones: { zone1: [{ type: "Comp", props: { id: "1" } }] },
Expand All @@ -482,7 +484,7 @@ describe("Data reducer", () => {
describe("registerZone action", () => {
it("should register a zone that's been previously unregistered", () => {
const state: AppData = {
state: {},
state: defaultState,
data: {
...defaultData,
zones: { zone1: [{ type: "Comp", props: { id: "1" } }] },
Expand All @@ -509,7 +511,7 @@ describe("Data reducer", () => {

describe("set action", () => {
it("should set new data", () => {
const state: AppData = { state: {}, data: { ...defaultData } };
const state: AppData = { state: defaultState, data: { ...defaultData } };
const newData: Data = {
...defaultData,
root: { title: "Hello, world" },
Expand Down
38 changes: 38 additions & 0 deletions packages/core/reducer/__tests__/state.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { SetStateAction, createReducer } from "../../reducer";
import { AppData, AppState, Config, Data } from "../../types/Config";

type Props = {
Comp: {
prop: string;
};
};
const defaultData: Data = { root: { title: "" }, content: [], zones: {} };

const defaultState: AppState = { leftSideBarVisible: true };

describe("State reducer", () => {
const config: Config<Props> = {
components: {
Comp: {
defaultProps: { prop: "example" },
render: () => <div />,
},
},
};

const reducer = createReducer({ config });

describe("setState action", () => {
it("should insert data into the state", () => {
const state: AppData = { state: defaultState, data: defaultData };

const action: SetStateAction = {
type: "setState",
state: { leftSideBarVisible: false },
};

const newState = reducer(state, action);
expect(newState.state.leftSideBarVisible).toEqual(false);
});
});
});
14 changes: 13 additions & 1 deletion packages/core/reducer/actions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Data } from "../types/Config";
import { AppData, AppState, Data } from "../types/Config";

export type InsertAction = {
type: "insert";
Expand Down Expand Up @@ -41,11 +41,21 @@ export type RemoveAction = {
zone: string;
};

export type SetStateAction = {
type: "setState";
state: Partial<AppState>;
};

export type SetDataAction = {
type: "setData";
data: Partial<Data>;
};

export type SetAction = {
type: "set";
appData: Partial<AppData>;
};

export type RegisterZoneAction = {
type: "registerZone";
zone: string;
Expand All @@ -63,6 +73,8 @@ export type PuckAction =
| ReplaceAction
| RemoveAction
| DuplicateAction
| SetAction
| SetDataAction
| SetStateAction
| RegisterZoneAction
| UnregisterZoneAction;
14 changes: 12 additions & 2 deletions packages/core/reducer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AppData, Config } from "../types/Config";
import { recordDiff } from "../lib/use-puck-history";
import { reduceData } from "./data";
import { PuckAction } from "./actions";
import { reduceState } from "./state";

export * from "./actions";
export * from "./data";
Expand All @@ -15,7 +16,11 @@ const storeInterceptor = (reducer: StateReducer) => {
return (data, action) => {
const newAppData = reducer(data, action);

if (!["registerZone", "unregisterZone", "setData"].includes(action.type)) {
if (
!["registerZone", "unregisterZone", "setData", "set"].includes(
action.type
)
) {
recordDiff(newAppData);
}

Expand All @@ -26,6 +31,11 @@ const storeInterceptor = (reducer: StateReducer) => {
export const createReducer = ({ config }: { config: Config }): StateReducer =>
storeInterceptor((appData, action) => {
const data = reduceData(appData.data, action, config);
const state = reduceState(appData.state, action);

return { data, state: {} };
if (action.type === "set") {
return { ...appData, ...action.appData };
}

return { data, state: state };
});
13 changes: 13 additions & 0 deletions packages/core/reducer/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { AppState } from "../types/Config";
import { PuckAction } from "./actions";

export const reduceState = (state: AppState, action: PuckAction) => {
if (action.type === "setState") {
return {
...state,
...action.state,
};
}

return state;
};
Loading

0 comments on commit 0f2d7c5

Please sign in to comment.