From ed28242a628233545e975aeb463b2f44abe6a731 Mon Sep 17 00:00:00 2001 From: Chris Villa Date: Mon, 20 Jan 2025 19:00:49 +0000 Subject: [PATCH 1/3] docs: fix typo in permissions docs --- .../pages/docs/api-reference/configuration/component-config.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/docs/pages/docs/api-reference/configuration/component-config.mdx b/apps/docs/pages/docs/api-reference/configuration/component-config.mdx index f3696cea7c..b980c9aa2d 100644 --- a/apps/docs/pages/docs/api-reference/configuration/component-config.mdx +++ b/apps/docs/pages/docs/api-reference/configuration/component-config.mdx @@ -572,7 +572,7 @@ An object describing the [AppState](/docs/api-reference/app-state). An object describing which props have changed on this component since the last time this function was called. This helps prevent duplicate calls when making async operations. ```tsx copy {2-4} /changed/1 filename="Example only updating the permissions when 'example' prop changes" -const resolveFields = async ({ props }, { changed, lastPermissions }) => { +const resolvePermissions = async ({ props }, { changed, lastPermissions }) => { if (!changed.example) { return lastPermissions; // Return the last permissions unless the `example` prop has changed } From 643c5bc68c6f1ffbdf904473b62b89568f4bf78f Mon Sep 17 00:00:00 2001 From: Chris Villa Date: Mon, 20 Jan 2025 19:01:21 +0000 Subject: [PATCH 2/3] refactor: move path data behaviour to separate lib --- .../core/components/DragDropContext/index.tsx | 48 ++----------------- packages/core/lib/use-path-data.ts | 46 ++++++++++++++++++ 2 files changed, 50 insertions(+), 44 deletions(-) create mode 100644 packages/core/lib/use-path-data.ts diff --git a/packages/core/components/DragDropContext/index.tsx b/packages/core/components/DragDropContext/index.tsx index ab6d68496e..9d5bb25f5f 100644 --- a/packages/core/components/DragDropContext/index.tsx +++ b/packages/core/components/DragDropContext/index.tsx @@ -16,14 +16,8 @@ import { AutoScroller, defaultPreset, DragDropManager } from "@dnd-kit/dom"; import { DragDropEvents } from "@dnd-kit/abstract"; import { DropZoneProvider } from "../DropZone"; import type { Draggable, Droppable } from "@dnd-kit/dom"; -import { getItem, ItemSelector } from "../../lib/get-item"; -import { - PathData, - Preview, - ZoneStore, - ZoneStoreProvider, -} from "../DropZone/context"; -import { getZoneId } from "../../lib/get-zone-id"; +import { getItem } from "../../lib/get-item"; +import { Preview, ZoneStore, ZoneStoreProvider } from "../DropZone/context"; import { createNestedDroppablePlugin } from "../../lib/dnd/NestedDroppablePlugin"; import { insertComponent } from "../../lib/insert-component"; import { useDebouncedCallback } from "use-debounce"; @@ -35,6 +29,7 @@ import { PointerSensor } from "../../lib/dnd/PointerSensor"; import { collisionStore } from "../../lib/dnd/collision/dynamic/store"; import { generateId } from "../../lib/generate-id"; import { createStore } from "zustand"; +import { usePathData } from "../../lib/use-path-data"; const DEBUG = false; @@ -300,44 +295,9 @@ const DragDropContextClient = ({ const [dragListeners, setDragListeners] = useState({}); - const [pathData, setPathData] = useState(); - const dragMode = useRef<"new" | "existing" | null>(null); - const registerPath = useCallback( - (id: string, selector: ItemSelector, label: string) => { - const [area] = getZoneId(selector.zone); - - setPathData((latestPathData = {}) => { - const parentPathData = latestPathData[area] || { path: [] }; - - return { - ...latestPathData, - [id]: { - path: [ - ...parentPathData.path, - ...(selector.zone ? [selector.zone] : []), - ], - label: label, - }, - }; - }); - }, - [data, setPathData] - ); - - const unregisterPath = useCallback( - (id: string) => { - setPathData((latestPathData = {}) => { - const newPathData = { ...latestPathData }; - - delete newPathData[id]; - - return newPathData; - }); - }, - [data, setPathData] - ); + const { pathData, registerPath, unregisterPath } = usePathData(data); const initialSelector = useRef<{ zone: string; index: number }>(undefined); diff --git a/packages/core/lib/use-path-data.ts b/packages/core/lib/use-path-data.ts new file mode 100644 index 0000000000..0ded4bd16b --- /dev/null +++ b/packages/core/lib/use-path-data.ts @@ -0,0 +1,46 @@ +import { useCallback, useState } from "react"; +import { PathData } from "../components/DropZone/context"; +import { ItemSelector } from "./get-item"; +import { getZoneId } from "./get-zone-id"; +import { Data } from "../types"; + +export const usePathData = (data: Data) => { + const [pathData, setPathData] = useState(); + + const registerPath = useCallback( + (id: string, selector: ItemSelector, label: string) => { + const [area] = getZoneId(selector.zone); + + setPathData((latestPathData = {}) => { + const parentPathData = latestPathData[area] || { path: [] }; + + return { + ...latestPathData, + [id]: { + path: [ + ...parentPathData.path, + ...(selector.zone ? [selector.zone] : []), + ], + label: label, + }, + }; + }); + }, + [data, setPathData] + ); + + const unregisterPath = useCallback( + (id: string) => { + setPathData((latestPathData = {}) => { + const newPathData = { ...latestPathData }; + + delete newPathData[id]; + + return newPathData; + }); + }, + [data, setPathData] + ); + + return { pathData, registerPath, unregisterPath }; +}; From 948c22ad217f344c6bde5aa6369f26d4f26ec47e Mon Sep 17 00:00:00 2001 From: Chris Villa Date: Mon, 20 Jan 2025 19:10:49 +0000 Subject: [PATCH 3/3] wip: add parent to resolvers --- .../core/components/DragDropContext/index.tsx | 8 +--- .../components/DraggableComponent/index.tsx | 6 ++- packages/core/components/DropZone/context.tsx | 3 -- packages/core/components/LayerTree/index.tsx | 4 +- packages/core/components/Puck/context.tsx | 17 ++++++- .../lib/__tests__/use-breadcrumbs.spec.tsx | 18 ++++---- packages/core/lib/is-child-of-zone.ts | 7 +-- packages/core/lib/use-breadcrumbs.ts | 6 +-- packages/core/lib/use-parent.ts | 46 +++++++++++++------ packages/core/lib/use-resolved-permissions.ts | 14 ++++-- packages/core/types/Config.tsx | 1 + 11 files changed, 85 insertions(+), 45 deletions(-) diff --git a/packages/core/components/DragDropContext/index.tsx b/packages/core/components/DragDropContext/index.tsx index 9d5bb25f5f..8edb49faf3 100644 --- a/packages/core/components/DragDropContext/index.tsx +++ b/packages/core/components/DragDropContext/index.tsx @@ -29,7 +29,6 @@ import { PointerSensor } from "../../lib/dnd/PointerSensor"; import { collisionStore } from "../../lib/dnd/collision/dynamic/store"; import { generateId } from "../../lib/generate-id"; import { createStore } from "zustand"; -import { usePathData } from "../../lib/use-path-data"; const DEBUG = false; @@ -104,7 +103,7 @@ const DragDropContextClient = ({ children, disableAutoScroll, }: DragDropContextProps) => { - const { state, config, dispatch, resolveData } = useAppContext(); + const { state, config, dispatch, resolveData, pathData } = useAppContext(); const id = useId(); @@ -297,8 +296,6 @@ const DragDropContextClient = ({ const dragMode = useRef<"new" | "existing" | null>(null); - const { pathData, registerPath, unregisterPath } = usePathData(data); - const initialSelector = useRef<{ zone: string; index: number }>(undefined); return ( @@ -550,9 +547,6 @@ const DragDropContextClient = ({ mode: "edit", areaId: "root", depth: 0, - registerPath, - unregisterPath, - pathData, path: [], }} > diff --git a/packages/core/components/DraggableComponent/index.tsx b/packages/core/components/DraggableComponent/index.tsx index 01efcbe823..076a7087a2 100644 --- a/packages/core/components/DraggableComponent/index.tsx +++ b/packages/core/components/DraggableComponent/index.tsx @@ -126,6 +126,8 @@ export const DraggableComponent = ({ dispatch, iframe, state, + registerPath, + unregisterPath, } = useAppContext(); const ctx = useContext(dropZoneContext); @@ -298,7 +300,7 @@ export const DraggableComponent = ({ }, [ref.current]); useEffect(() => { - ctx?.registerPath!( + registerPath!( id, { index, @@ -308,7 +310,7 @@ export const DraggableComponent = ({ ); return () => { - ctx?.unregisterPath?.(id); + unregisterPath?.(id); }; }, [id, zoneCompound, index, componentType]); diff --git a/packages/core/components/DropZone/context.tsx b/packages/core/components/DropZone/context.tsx index 34f76d5490..a700ae5056 100644 --- a/packages/core/components/DropZone/context.tsx +++ b/packages/core/components/DropZone/context.tsx @@ -31,9 +31,6 @@ export type DropZoneContext = { registerZone?: (zoneCompound: string) => void; unregisterZone?: (zoneCompound: string) => void; activeZones?: Record; - pathData?: PathData; - registerPath?: (id: string, selector: ItemSelector, label: string) => void; - unregisterPath?: (id: string) => void; mode?: "edit" | "render"; depth: number; registerLocalZone?: (zone: string, active: boolean) => void; // A zone as it pertains to the current area diff --git a/packages/core/components/LayerTree/index.tsx b/packages/core/components/LayerTree/index.tsx index 9adbe8deec..10040309f8 100644 --- a/packages/core/components/LayerTree/index.tsx +++ b/packages/core/components/LayerTree/index.tsx @@ -12,6 +12,7 @@ import { getZoneId } from "../../lib/get-zone-id"; import { isChildOfZone } from "../../lib/is-child-of-zone"; import { getFrame } from "../../lib/get-frame"; import { onScrollEnd } from "../../lib/on-scroll-end"; +import { useAppContext } from "../Puck/context"; const getClassName = getClassNameFactory("LayerTree", styles); const getClassNameLayer = getClassNameFactory("Layer", styles); @@ -35,6 +36,7 @@ export const LayerTree = ({ }) => { const zones = data.zones || {}; const ctx = useContext(dropZoneContext); + const appContext = useAppContext(); return ( <> @@ -67,7 +69,7 @@ export const LayerTree = ({ const isHovering = hoveringComponent === item.props.id; - const childIsSelected = isChildOfZone(item, selectedItem, ctx); + const childIsSelected = isChildOfZone(item, selectedItem, appContext); const componentConfig: ComponentConfig | undefined = config.components[item.type]; diff --git a/packages/core/components/Puck/context.tsx b/packages/core/components/Puck/context.tsx index 36ce8d24a5..199774a833 100644 --- a/packages/core/components/Puck/context.tsx +++ b/packages/core/components/Puck/context.tsx @@ -17,7 +17,7 @@ import { UserGenerics, } from "../../types"; import { PuckAction } from "../../reducer"; -import { getItem } from "../../lib/get-item"; +import { getItem, ItemSelector } from "../../lib/get-item"; import { PuckHistory } from "../../lib/use-puck-history"; import { defaultViewports } from "../ViewportControls/default-viewports"; import { Viewports } from "../../types"; @@ -27,6 +27,8 @@ import { useResolvedPermissions, } from "../../lib/use-resolved-permissions"; import { useResolvedData } from "../../lib/use-resolved-data"; +import { usePathData } from "../../lib/use-path-data"; +import { PathData } from "../DropZone/context"; export const defaultAppState: AppState = { data: { content: [], root: {} }, @@ -83,6 +85,9 @@ export type AppContext< selectedItem?: G["UserData"]["content"][0]; getPermissions: GetPermissions; refreshPermissions: RefreshPermissions; + pathData?: PathData; + registerPath?: (id: string, selector: ItemSelector, label: string) => void; + unregisterPath?: (id: string) => void; }; export const defaultContext: AppContext = { @@ -132,6 +137,10 @@ export const AppProvider = ({ const [status, setStatus] = useState("LOADING"); + const { pathData, registerPath, unregisterPath } = usePathData( + value.state.data + ); + // App is ready when client has loaded, after initial render // This triggers DropZones to activate useEffect(() => { @@ -171,7 +180,8 @@ export const AppProvider = ({ value.state, value.globalPermissions || {}, setComponentLoading, - unsetComponentLoading + unsetComponentLoading, + pathData ); const { resolveData } = useResolvedData( @@ -197,6 +207,9 @@ export const AppProvider = ({ componentState, setComponentState, resolveData, + pathData, + registerPath, + unregisterPath, }} > {children} diff --git a/packages/core/lib/__tests__/use-breadcrumbs.spec.tsx b/packages/core/lib/__tests__/use-breadcrumbs.spec.tsx index 8919f7ea2f..0670e624a8 100644 --- a/packages/core/lib/__tests__/use-breadcrumbs.spec.tsx +++ b/packages/core/lib/__tests__/use-breadcrumbs.spec.tsx @@ -1,4 +1,8 @@ -import { DropZoneContext } from "../../components/DropZone/context"; +import { + AppContext, + defaultAppState, + defaultContext, +} from "../../components/Puck/context"; import { Config, Data } from "../../types"; import { convertPathDataToBreadcrumbs } from "../use-breadcrumbs"; @@ -24,8 +28,9 @@ const config: Config = { }, }; -const dropzoneContext: DropZoneContext = { - data, +const appContext: AppContext = { + ...defaultContext, + state: { ...defaultAppState, data }, config, pathData: { "MyComponent-1": { path: [], label: "MyComponent" }, @@ -35,16 +40,13 @@ const dropzoneContext: DropZoneContext = { label: "MyComponent", }, }, - depth: 0, - path: [], }; describe("use-breadcrumbs", () => { describe("convert-path-data-to-breadcrumbs", () => { it("should convert path data to breadcrumbs", () => { - expect( - convertPathDataToBreadcrumbs(item3, dropzoneContext.pathData, data) - ).toMatchInlineSnapshot(` + expect(convertPathDataToBreadcrumbs(item3, appContext.pathData, data)) + .toMatchInlineSnapshot(` [ { "label": "MyComponent", diff --git a/packages/core/lib/is-child-of-zone.ts b/packages/core/lib/is-child-of-zone.ts index b1c414782c..f8749b9813 100644 --- a/packages/core/lib/is-child-of-zone.ts +++ b/packages/core/lib/is-child-of-zone.ts @@ -1,4 +1,5 @@ import { DropZoneContext } from "../components/DropZone/context"; +import { AppContext } from "../components/Puck/context"; import { Content } from "../types"; import { getItem } from "./get-item"; import { getZoneId } from "./get-zone-id"; @@ -6,11 +7,11 @@ import { getZoneId } from "./get-zone-id"; export const isChildOfZone = ( item: Content[0], maybeChild: Content[0] | null | undefined, - ctx: DropZoneContext + ctx: AppContext ) => { - const { data, pathData = {} } = ctx || {}; + const { state, pathData = {} } = ctx || {}; - return maybeChild && data + return maybeChild && state.data ? !!pathData[maybeChild.props.id]?.path.find((zoneCompound) => { const [area] = getZoneId(zoneCompound); diff --git a/packages/core/lib/use-breadcrumbs.ts b/packages/core/lib/use-breadcrumbs.ts index 09f4ff143f..1abe002ec9 100644 --- a/packages/core/lib/use-breadcrumbs.ts +++ b/packages/core/lib/use-breadcrumbs.ts @@ -81,13 +81,13 @@ export const useBreadcrumbs = (renderCount?: number) => { const { state: { data }, selectedItem, + pathData, } = useAppContext(); - const dzContext = useContext(dropZoneContext); return useMemo(() => { const breadcrumbs = convertPathDataToBreadcrumbs( selectedItem, - dzContext?.pathData, + pathData, data ); @@ -96,5 +96,5 @@ export const useBreadcrumbs = (renderCount?: number) => { } return breadcrumbs; - }, [selectedItem, dzContext?.pathData, renderCount]); + }, [selectedItem, pathData, renderCount]); }; diff --git a/packages/core/lib/use-parent.ts b/packages/core/lib/use-parent.ts index 692cd7641c..4bee84f10b 100644 --- a/packages/core/lib/use-parent.ts +++ b/packages/core/lib/use-parent.ts @@ -1,19 +1,15 @@ -import { useCallback, useContext } from "react"; +import { useCallback } from "react"; import { useAppContext } from "../components/Puck/context"; import { getItem, ItemSelector } from "./get-item"; -import { dropZoneContext } from "../components/DropZone"; import { convertPathDataToBreadcrumbs } from "./use-breadcrumbs"; import { PathData } from "../components/DropZone/context"; -import { Data } from "../types"; +import { ComponentData, Data } from "../types"; -export const getParent = ( - itemSelector: ItemSelector | null, +export const getParentByItem = ( + item: ComponentData | undefined, pathData: PathData | undefined, data: Data ) => { - if (!itemSelector) return null; - - const item = getItem(itemSelector, data); const breadcrumbs = convertPathDataToBreadcrumbs(item, pathData, data); const lastItem = breadcrumbs[breadcrumbs.length - 1]; @@ -24,16 +20,40 @@ export const getParent = ( return parent || null; }; +export const getParent = ( + itemSelector: ItemSelector | null, + pathData: PathData | undefined, + data: Data +) => { + if (!itemSelector) return null; + + const item = getItem(itemSelector, data); + + return getParentByItem(item, pathData, data); +}; + export const useGetParent = () => { - const { state } = useAppContext(); - const { pathData } = useContext(dropZoneContext) || {}; + const { state, pathData } = useAppContext(); + + return useCallback( + (itemSelector: ItemSelector | null) => + getParent(itemSelector, pathData, state.data), + [pathData, state.data] + ); +}; + +export const useGetParentByItem = () => { + const { state, pathData } = useAppContext(); return useCallback( - () => getParent(state.ui.itemSelector, pathData, state.data), - [state.ui.itemSelector, pathData, state.data] + (item: ComponentData | undefined) => + getParentByItem(item, pathData, state.data), + [pathData, state.data] ); }; export const useParent = () => { - return useGetParent()(); + const { state } = useAppContext(); + + return useGetParent()(state.ui.itemSelector); }; diff --git a/packages/core/lib/use-resolved-permissions.ts b/packages/core/lib/use-resolved-permissions.ts index af7403dcd9..532cc518f8 100644 --- a/packages/core/lib/use-resolved-permissions.ts +++ b/packages/core/lib/use-resolved-permissions.ts @@ -2,6 +2,12 @@ import { useCallback, useEffect, useState } from "react"; import { flattenData } from "./flatten-data"; import { ComponentData, Config, Permissions, UserGenerics } from "../types"; import { getChanged } from "./get-changed"; +import { + getParentByItem, + useGetParent, + useGetParentByItem, +} from "./use-parent"; +import { PathData } from "../components/DropZone/context"; type PermissionsArgs< UserConfig extends Config = Config, @@ -42,7 +48,8 @@ export const useResolvedPermissions = < appState: G["UserAppState"], globalPermissions: Partial, setComponentLoading?: (id: string) => void, - unsetComponentLoading?: (id: string) => void + unsetComponentLoading?: (id: string) => void, + pathData?: PathData ) => { const [cache, setCache] = useState({}); @@ -78,6 +85,7 @@ export const useResolvedPermissions = < permissions: initialPermissions, appState, lastData: cache[item.props.id]?.lastData || null, + parent: getParentByItem(item, pathData, appState.data), } ); @@ -98,7 +106,7 @@ export const useResolvedPermissions = < } } }, - [config, globalPermissions, appState, cache] + [config, globalPermissions, appState, cache, pathData] ); const resolveDataForRoot = (force = false) => { @@ -149,7 +157,7 @@ export const useResolvedPermissions = < resolvePermissions(); // We only trigger this effect on appState.data to avoid triggering on all UI changes - }, [config, appState.data]); + }, [config, appState.data, pathData]); const getPermissions: GetPermissions = useCallback( ({ item, type, root } = {}) => { diff --git a/packages/core/types/Config.tsx b/packages/core/types/Config.tsx index 183165bea2..f7b60baa3b 100644 --- a/packages/core/types/Config.tsx +++ b/packages/core/types/Config.tsx @@ -56,6 +56,7 @@ export type ComponentConfig< permissions: Partial; appState: AppState; lastData: DataShape | null; + parent: ComponentData | null; } ) => Promise> | Partial; };