diff --git a/packages/core/components/DropZone/context.tsx b/packages/core/components/DropZone/context.tsx index 900763d915..80e73404b6 100644 --- a/packages/core/components/DropZone/context.tsx +++ b/packages/core/components/DropZone/context.tsx @@ -13,10 +13,7 @@ import { rootDroppableId } from "../../lib/root-droppable-id"; import { useDebounce } from "use-debounce"; import { getZoneId } from "../../lib/get-zone-id"; -type PathData = Record< - string, - { selector: ItemSelector | null; label: string }[] ->; +export type PathData = Record; export type DropZoneContext = { data: Data; @@ -129,17 +126,17 @@ export const DropZoneProvider = ({ const [area] = getZoneId(selector.zone); setPathData((latestPathData = {}) => { - const pathData = latestPathData[area] || []; + const parentPathData = latestPathData[area] || { path: [] }; return { ...latestPathData, - [item.props.id]: [ - ...pathData, - { - selector, - label: item.type as string, - }, - ], + [item.props.id]: { + path: [ + ...parentPathData.path, + ...(selector.zone ? [selector.zone] : []), + ], + label: item.type as string, + }, }; }); }, @@ -164,7 +161,6 @@ export const DropZoneProvider = ({ activeZones, registerPath, pathData, - ...value, }} > diff --git a/packages/core/components/Puck/context.tsx b/packages/core/components/Puck/context.tsx index 04c41ff75d..05fbb2fe10 100644 --- a/packages/core/components/Puck/context.tsx +++ b/packages/core/components/Puck/context.tsx @@ -1,6 +1,7 @@ import { createContext, useContext } from "react"; import { AppData, AppState } from "../../types/Config"; import { PuckAction } from "../../reducer"; +import { getItem } from "../../lib/get-item"; export const defaultAppData: AppData = { data: { content: [], root: { title: "" } }, @@ -25,9 +26,14 @@ export const AppProvider = appContext.Provider; export const useAppContext = () => { const mainContext = useContext(appContext); + const selectedItem = mainContext.appData.state.itemSelector + ? getItem(mainContext.appData.state.itemSelector, mainContext.appData.data) + : undefined; + return { ...mainContext, - // Helper + // Helpers + selectedItem, setState: (state: Partial, recordHistory?: boolean) => { return mainContext.dispatch({ type: "setState", diff --git a/packages/core/components/Puck/index.tsx b/packages/core/components/Puck/index.tsx index ba1528795a..d1bff6cadc 100644 --- a/packages/core/components/Puck/index.tsx +++ b/packages/core/components/Puck/index.tsx @@ -265,16 +265,6 @@ export function Puck({ > {(ctx) => { - let path = - ctx?.pathData && selectedItem - ? ctx?.pathData[selectedItem?.props.id] - : undefined; - - if (path) { - path = [{ label: "Page", selector: null }, ...path]; - path = path.slice(path.length - 2, path.length - 1); - } - return (
- setItemSelector(breadcrumb.selector) - } + showBreadcrumbs title={selectedItem ? selectedItem.type : "Page"} > {Object.keys(fields).map((fieldName) => { diff --git a/packages/core/components/SidebarSection/index.tsx b/packages/core/components/SidebarSection/index.tsx index e9ee243bfb..f3115156f8 100644 --- a/packages/core/components/SidebarSection/index.tsx +++ b/packages/core/components/SidebarSection/index.tsx @@ -3,42 +3,46 @@ import styles from "./styles.module.css"; import getClassNameFactory from "../../lib/get-class-name-factory"; import { Heading } from "../Heading"; import { ChevronRight } from "react-feather"; -import { ItemSelector } from "../../lib/get-item"; +import { useBreadcrumbs } from "../../lib/use-breadcrumbs"; +import { useAppContext } from "../Puck/context"; const getClassName = getClassNameFactory("SidebarSection", styles); -type Breadcrumb = { label: string; selector: ItemSelector | null }; - export const SidebarSection = ({ children, title, background, - breadcrumbs = [], - breadcrumbClick, + showBreadcrumbs, noPadding, }: { children: ReactNode; title: ReactNode; background?: string; - breadcrumbs?: Breadcrumb[]; - breadcrumbClick?: (breadcrumb: Breadcrumb) => void; + showBreadcrumbs?: boolean; noPadding?: boolean; }) => { + const { setState } = useAppContext(); + const breadcrumbs = useBreadcrumbs(1); + return (
- {breadcrumbs.map((breadcrumb, i) => ( -
-
breadcrumbClick && breadcrumbClick(breadcrumb)} - > - {breadcrumb.label} -
- -
- ))} + {showBreadcrumbs + ? breadcrumbs.map((breadcrumb, i) => ( +
+
+ setState({ itemSelector: breadcrumb.selector }) + } + > + {breadcrumb.label} +
+ +
+ )) + : null}
{title} diff --git a/packages/core/lib/__tests__/is-child-of-zone.spec.tsx b/packages/core/lib/__tests__/is-child-of-zone.spec.tsx new file mode 100644 index 0000000000..90fdd2e127 --- /dev/null +++ b/packages/core/lib/__tests__/is-child-of-zone.spec.tsx @@ -0,0 +1,52 @@ +import { DropZoneContext } from "../../components/DropZone/context"; +import { Config, Data } from "../../types/Config"; +import { isChildOfZone } from "../is-child-of-zone"; + +const item1 = { type: "MyComponent", props: { id: "MyComponent-1" } }; +const item2 = { type: "MyComponent", props: { id: "MyComponent-2" } }; +const item3 = { type: "MyComponent", props: { id: "MyComponent-3" } }; + +const data: Data = { + root: { title: "" }, + content: [item1], + zones: { + "MyComponent-1:zone": [item2], + "MyComponent-2:zone": [item3], + }, +}; + +const config: Config = { + components: { + Comp: { + defaultProps: { prop: "example" }, + render: () =>
, + }, + }, +}; + +const dropzoneContext: DropZoneContext = { + data, + config, + pathData: { + "MyComponent-1": { path: [], label: "MyComponent" }, + "MyComponent-2": { path: ["MyComponent-1:zone"], label: "MyComponent" }, + "MyComponent-3": { + path: ["MyComponent-1:zone", "MyComponent-2:zone"], + label: "MyComponent", + }, + }, +}; + +describe("is-child-of-zone", () => { + it("should return true when item is child of zone for first item", () => { + expect(isChildOfZone(item1, item2, dropzoneContext)).toBe(true); + }); + + it("should return true when item is child of zone for second item", () => { + expect(isChildOfZone(item2, item3, dropzoneContext)).toBe(true); + }); + + it("should return false when item is not child of zone", () => { + expect(isChildOfZone(item2, item1, dropzoneContext)).toBe(false); + }); +}); diff --git a/packages/core/lib/__tests__/use-breadcrumbs.spec.tsx b/packages/core/lib/__tests__/use-breadcrumbs.spec.tsx new file mode 100644 index 0000000000..9188678a14 --- /dev/null +++ b/packages/core/lib/__tests__/use-breadcrumbs.spec.tsx @@ -0,0 +1,67 @@ +import { DropZoneContext } from "../../components/DropZone/context"; +import { Config, Data } from "../../types/Config"; +import { Breadcrumb, convertPathDataToBreadcrumbs } from "../use-breadcrumbs"; + +const item1 = { type: "MyComponent", props: { id: "MyComponent-1" } }; +const item2 = { type: "MyComponent", props: { id: "MyComponent-2" } }; +const item3 = { type: "MyComponent", props: { id: "MyComponent-3" } }; + +const data: Data = { + root: { title: "" }, + content: [item1], + zones: { + "MyComponent-1:zone": [item2], + "MyComponent-2:zone": [item3], + }, +}; + +const config: Config = { + components: { + Comp: { + defaultProps: { prop: "example" }, + render: () =>
, + }, + }, +}; + +const dropzoneContext: DropZoneContext = { + data, + config, + pathData: { + "MyComponent-1": { path: [], label: "MyComponent" }, + "MyComponent-2": { path: ["MyComponent-1:zone"], label: "MyComponent" }, + "MyComponent-3": { + path: ["MyComponent-1:zone", "MyComponent-2:zone"], + label: "MyComponent", + }, + }, +}; + +describe("use-breadcrumbs", () => { + describe("convert-path-data-to-breadcrumbs", () => { + it("should convert path data to breadcrumbs", () => { + expect( + convertPathDataToBreadcrumbs(item3, dropzoneContext.pathData, data) + ).toMatchInlineSnapshot(` + [ + { + "label": "MyComponent", + "selector": { + "index": 0, + "zone": "default-zone", + }, + "zoneCompound": "MyComponent-1:zone", + }, + { + "label": "MyComponent", + "selector": { + "index": 0, + "zone": "MyComponent-1:zone", + }, + "zoneCompound": "MyComponent-2:zone", + }, + ] + `); + }); + }); +}); diff --git a/packages/core/lib/get-zone-id.ts b/packages/core/lib/get-zone-id.ts index 6aa0ce8b59..e6666ec279 100644 --- a/packages/core/lib/get-zone-id.ts +++ b/packages/core/lib/get-zone-id.ts @@ -1,3 +1,5 @@ +import { rootDroppableId } from "./root-droppable-id"; + export const getZoneId = (zoneCompound?: string) => { if (!zoneCompound) { return []; @@ -7,5 +9,5 @@ export const getZoneId = (zoneCompound?: string) => { return zoneCompound.split(":"); } - return ["root", zoneCompound]; + return [rootDroppableId, zoneCompound]; }; diff --git a/packages/core/lib/is-child-of-zone.ts b/packages/core/lib/is-child-of-zone.ts index a6e66c6114..f97008b74f 100644 --- a/packages/core/lib/is-child-of-zone.ts +++ b/packages/core/lib/is-child-of-zone.ts @@ -1,6 +1,7 @@ import { DropZoneContext } from "../components/DropZone/context"; import { Content } from "../types/Config"; import { getItem } from "./get-item"; +import { getZoneId } from "./get-zone-id"; export const isChildOfZone = ( item: Content[0], @@ -10,14 +11,10 @@ export const isChildOfZone = ( const { data, pathData = {} } = ctx || {}; return maybeChild && data - ? !!pathData[maybeChild.props.id]?.find((path) => { - if (path.selector) { - const pathItem = getItem(path.selector!, data); + ? !!pathData[maybeChild.props.id]?.path.find((zoneCompound) => { + const [area] = getZoneId(zoneCompound); - return pathItem?.props.id === item.props.id; - } - - return false; + return area === item.props.id; }) : false; }; diff --git a/packages/core/lib/use-breadcrumbs.ts b/packages/core/lib/use-breadcrumbs.ts new file mode 100644 index 0000000000..9e33eb4f30 --- /dev/null +++ b/packages/core/lib/use-breadcrumbs.ts @@ -0,0 +1,100 @@ +import { useContext, useMemo } from "react"; +import { dropZoneContext, PathData } from "../components/DropZone/context"; +import { useAppContext } from "../components/Puck/context"; +import { getZoneId } from "./get-zone-id"; +import { rootDroppableId } from "./root-droppable-id"; +import { ItemSelector } from "./get-item"; +import { Data, MappedItem } from "../types/Config"; + +export type Breadcrumb = { + label: string; + selector: ItemSelector | null; + zoneCompound?: string; +}; + +export const convertPathDataToBreadcrumbs = ( + selectedItem: MappedItem | undefined, + pathData: PathData | undefined, + data: Data +) => { + const id = selectedItem ? selectedItem?.props.id : ""; + + const currentPathData = + pathData && id && pathData[id] + ? { ...pathData[id] } + : { label: "Page", path: [] }; + + if (!id) { + return []; + } + + return currentPathData?.path.reduce((acc, zoneCompound) => { + const [area] = getZoneId(zoneCompound); + + if (area === rootDroppableId) { + return [ + { + label: "Page", + selector: null, + }, + ]; + } + + const parentZoneCompound = + acc.length > 0 ? acc[acc.length - 1].zoneCompound : rootDroppableId; + + let parentZone = data.content; + + if (parentZoneCompound && parentZoneCompound !== rootDroppableId) { + parentZone = data.zones![parentZoneCompound]; + } + + if (!parentZone) { + return acc; + } + + const itemIndex = parentZone.findIndex( + (queryItem) => queryItem.props.id === area + ); + + const item = parentZone[itemIndex]; + + if (!item) { + return acc; + } + + return [ + ...acc, + { + label: item.type.toString(), + selector: { + index: itemIndex, + zone: parentZoneCompound, + }, + zoneCompound: zoneCompound, + }, + ]; + }, []); +}; + +export const useBreadcrumbs = (renderCount?: number) => { + const { + appData: { data }, + selectedItem, + } = useAppContext(); + const dzContext = useContext(dropZoneContext); + + return useMemo(() => { + const breadcrumbs = convertPathDataToBreadcrumbs( + selectedItem, + dzContext?.pathData, + data + ); + + if (renderCount) { + return breadcrumbs.slice(breadcrumbs.length - renderCount); + } + + return breadcrumbs; + }, [selectedItem, dzContext?.pathData, renderCount]); +}; diff --git a/packages/core/types/Config.tsx b/packages/core/types/Config.tsx index 5f359b29dc..1f2dc4aa39 100644 --- a/packages/core/types/Config.tsx +++ b/packages/core/types/Config.tsx @@ -94,13 +94,14 @@ export type Config< >; }; -type MappedItem = - { - type: keyof Props; - props: WithId<{ - [key: string]: any; - }>; - }; +export type MappedItem< + Props extends { [key: string]: any } = { [key: string]: any } +> = { + type: keyof Props; + props: WithId<{ + [key: string]: any; + }>; +}; export type Data< Props extends { [key: string]: any } = { [key: string]: any },