diff --git a/packages/core/components/LayerTree/index.tsx b/packages/core/components/LayerTree/index.tsx index 14fcf8180c..ba29d19947 100644 --- a/packages/core/components/LayerTree/index.tsx +++ b/packages/core/components/LayerTree/index.tsx @@ -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; diff --git a/packages/core/components/Puck/index.tsx b/packages/core/components/Puck/index.tsx index 4efa7fec10..5f1cd34a38 100644 --- a/packages/core/components/Puck/index.tsx +++ b/packages/core/components/Puck/index.tsx @@ -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( reducer, flushZones(initialAppData) ); - const { data } = appData; + const { data, state } = appData; const { canForward, canRewind, rewind, forward } = usePuckHistory({ appData, dispatch, }); - const [itemSelector, setItemSelector] = useState(null); + const { itemSelector, leftSideBarVisible } = state; + + const setItemSelector = useCallback( + (newItemSelector: ItemSelector | null) => { + dispatch({ type: "setState", state: { itemSelector: newItemSelector } }); + }, + [] + ); const selectedItem = itemSelector ? getItem(itemSelector, data) : null; @@ -164,8 +174,6 @@ export function Puck({ const { onDragStartOrUpdate, placeholderStyle } = usePlaceholderStyle(); - const [leftSidebarVisible, setLeftSidebarVisible] = useState(true); - const [draggedItem, setDraggedItem] = useState< DragStart & Partial >(); @@ -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", @@ -318,7 +326,12 @@ export function Puck({ > - setLeftSidebarVisible(!leftSidebarVisible) + dispatch({ + type: "setState", + state: { + leftSideBarVisible: !leftSideBarVisible, + }, + }) } title="Toggle left sidebar" > diff --git a/packages/core/lib/use-puck-history.ts b/packages/core/lib/use-puck-history.ts index 061f261bb9..b3b124259b 100644 --- a/packages/core/lib/use-puck-history.ts +++ b/packages/core/lib/use-puck-history.ts @@ -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); @@ -51,7 +51,7 @@ export function usePuckHistory({ return target; }, target); - dispatch({ type: "setData", data: target.data }); + dispatch({ type: "set", appData: target }); }, }); }, DEBOUNCE_TIME); diff --git a/packages/core/reducer/__tests__/data.spec.tsx b/packages/core/reducer/__tests__/data.spec.tsx index 4813aa6a76..2ec967568f 100644 --- a/packages/core/reducer/__tests__/data.spec.tsx +++ b/packages/core/reducer/__tests__/data.spec.tsx @@ -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"; @@ -26,6 +26,8 @@ type Props = { }; const defaultData: Data = { root: { title: "" }, content: [], zones: {} }; +const defaultState: AppState = { leftSideBarVisible: true }; + describe("Data reducer", () => { const config: Config = { components: { @@ -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", @@ -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", @@ -76,7 +78,7 @@ describe("Data reducer", () => { describe("reorder action", () => { it("should reorder within rootDroppableId", () => { const state: AppData = { - state: {}, + state: defaultState, data: { ...defaultData, content: [ @@ -99,7 +101,7 @@ describe("Data reducer", () => { it("should reorder within a different zone", () => { const state: AppData = { - state: {}, + state: defaultState, data: { ...defaultData, zones: { @@ -126,7 +128,7 @@ describe("Data reducer", () => { describe("duplicate action", () => { it("should duplicate in content", () => { const state: AppData = { - state: {}, + state: defaultState, data: { ...defaultData, content: [ @@ -151,7 +153,7 @@ describe("Data reducer", () => { it("should duplicate in a different zone", () => { const state: AppData = { - state: {}, + state: defaultState, data: { ...defaultData, zones: { @@ -184,7 +186,7 @@ describe("Data reducer", () => { mockedGenerateId.mockImplementation(() => `mockId-${counter++}`); const state: AppData = { - state: {}, + state: defaultState, data: { ...defaultData, zones: { @@ -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" } }], @@ -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" } }], @@ -306,7 +308,7 @@ describe("Data reducer", () => { it("should move between two zones", () => { const state: AppData = { - state: {}, + state: defaultState, data: { ...defaultData, content: [], @@ -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" } }], @@ -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" } }] }, @@ -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" } }], @@ -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" } }] }, @@ -417,7 +419,7 @@ describe("Data reducer", () => { mockedGenerateId.mockImplementation(() => `mockId-${counter++}`); const state: AppData = { - state: {}, + state: defaultState, data: { ...defaultData, zones: { @@ -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" } }] }, @@ -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" } }] }, @@ -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" }, diff --git a/packages/core/reducer/__tests__/state.spec.tsx b/packages/core/reducer/__tests__/state.spec.tsx new file mode 100644 index 0000000000..4459aa5cba --- /dev/null +++ b/packages/core/reducer/__tests__/state.spec.tsx @@ -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 = { + components: { + Comp: { + defaultProps: { prop: "example" }, + render: () =>
, + }, + }, + }; + + 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); + }); + }); +}); diff --git a/packages/core/reducer/actions.tsx b/packages/core/reducer/actions.tsx index 45b175dff3..ac2bf8bd25 100644 --- a/packages/core/reducer/actions.tsx +++ b/packages/core/reducer/actions.tsx @@ -1,4 +1,4 @@ -import { Data } from "../types/Config"; +import { AppData, AppState, Data } from "../types/Config"; export type InsertAction = { type: "insert"; @@ -41,11 +41,21 @@ export type RemoveAction = { zone: string; }; +export type SetStateAction = { + type: "setState"; + state: Partial; +}; + export type SetDataAction = { type: "setData"; data: Partial; }; +export type SetAction = { + type: "set"; + appData: Partial; +}; + export type RegisterZoneAction = { type: "registerZone"; zone: string; @@ -63,6 +73,8 @@ export type PuckAction = | ReplaceAction | RemoveAction | DuplicateAction + | SetAction | SetDataAction + | SetStateAction | RegisterZoneAction | UnregisterZoneAction; diff --git a/packages/core/reducer/index.ts b/packages/core/reducer/index.ts index c197114914..01af35d0ec 100644 --- a/packages/core/reducer/index.ts +++ b/packages/core/reducer/index.ts @@ -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"; @@ -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); } @@ -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 }; }); diff --git a/packages/core/reducer/state.ts b/packages/core/reducer/state.ts new file mode 100644 index 0000000000..68e31be1f2 --- /dev/null +++ b/packages/core/reducer/state.ts @@ -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; +}; diff --git a/packages/core/types/Config.tsx b/packages/core/types/Config.tsx index 6d251a0e8b..e13ec4c456 100644 --- a/packages/core/types/Config.tsx +++ b/packages/core/types/Config.tsx @@ -1,5 +1,7 @@ import { ReactElement } from "react"; import { ReactNode } from "react"; +import { ItemSelector } from "../lib/get-item"; +import { DragStart, DragUpdate } from "react-beautiful-dnd"; export type Adaptor = { name: string; @@ -112,4 +114,11 @@ export type Data< zones?: Record>; }; -export type AppData = { data: Data; state: {} }; +type DraggedItem = DragStart & Partial; + +export type AppState = { + leftSideBarVisible: boolean; + itemSelector?: ItemSelector | null; +}; + +export type AppData = { data: Data; state: AppState };