From 35b8e16bafef289ebe64e4005458dfa3084b2327 Mon Sep 17 00:00:00 2001 From: heswell Date: Mon, 2 Oct 2023 13:21:36 +0100 Subject: [PATCH] add resolveJSONPath utility method (#887) --- .../src/layout-provider/LayoutProvider.tsx | 38 ++++++++++++-- .../src/layout-reducer/layoutTypes.ts | 44 ++++++++++++++++ .../vuu-layout/src/stack/StackLayout.tsx | 2 +- .../vuu-layout/src/utils/pathUtils.ts | 52 ++++++++++++++++++- .../src/layout-config/local-config.ts | 6 ++- vuu-ui/packages/vuu-shell/src/shell.tsx | 10 ++-- 6 files changed, 141 insertions(+), 11 deletions(-) diff --git a/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx b/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx index 4a896857f..12f167e44 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx +++ b/vuu-ui/packages/vuu-layout/src/layout-provider/LayoutProvider.tsx @@ -9,12 +9,15 @@ import { } from "react"; import { LayoutActionType, + LayoutChangeHandler, + LayoutChangeReason, layoutFromJson, LayoutJSON, layoutReducer, LayoutReducerAction, layoutToJSON, processLayoutElement, + SaveAction, } from "../layout-reducer"; import { findTarget, getChildProp, getProp, getProps, typeOf } from "../utils"; import { @@ -34,7 +37,31 @@ const shouldSave = (action: LayoutReducerAction) => "switch-tab", ].includes(action.type); -type LayoutChangeHandler = (layout: LayoutJSON, source: string) => void; +const getLayoutChangeReason = ( + action: LayoutReducerAction | SaveAction +): LayoutChangeReason => { + switch (action.type) { + case "switch-tab": + // TODO how can we make this more robust, shouldn't rely on 'main-tabs' + if (action.id === "main-tabs") { + return "switch-active-layout"; + } else { + return "switch-active-tab"; + } + case "save": + return "save-feature-props"; + case "drag-drop": + return "drag-drop-operation"; + case "remove": + return "remove-component"; + case "splitter-resize": + return "resize-component"; + case "set-title": + return "edit-feature-title"; + default: + throw Error("unknown layout action"); + } +}; export interface LayoutProviderProps { children: ReactElement; @@ -58,7 +85,8 @@ export const LayoutProvider = (props: LayoutProviderProps): ReactElement => { const [, forceRefresh] = useState(null); const serializeState = useCallback( - (source) => { + (source, layoutChangeReason: LayoutChangeReason) => { + console.log(`serialize state ${layoutChangeReason}`); if (onLayoutChange) { const targetContainer = findTarget(source, withDropTarget) || state.current; @@ -67,7 +95,7 @@ export const LayoutProvider = (props: LayoutProviderProps): ReactElement => { ? getProps(targetContainer).children[0] : targetContainer; const serializedModel = layoutToJSON(target); - onLayoutChange(serializedModel, "drag-root"); + onLayoutChange(serializedModel, layoutChangeReason); } }, [onLayoutChange] @@ -80,7 +108,7 @@ export const LayoutProvider = (props: LayoutProviderProps): ReactElement => { state.current = nextState; forceRefresh({}); if (!suppressSave && shouldSave(action)) { - serializeState(nextState); + serializeState(nextState, getLayoutChangeReason(action)); } } }, @@ -95,7 +123,7 @@ export const LayoutProvider = (props: LayoutProviderProps): ReactElement => { break; } case "save": { - serializeState(state.current); + serializeState(state.current, getLayoutChangeReason(action)); break; } default: { diff --git a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts index 81c602eb2..492fad9cc 100644 --- a/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts +++ b/vuu-ui/packages/vuu-layout/src/layout-reducer/layoutTypes.ts @@ -135,6 +135,7 @@ export type LayoutResizeAction = { }; export type SwitchTabAction = { + id?: string; nextIdx: number; path: string; type: typeof LayoutActionType.SWITCH_TAB; @@ -193,3 +194,46 @@ export type DragStartAction = { path: string; type: typeof LayoutActionType.DRAG_START; }; + +export type LayoutLevelChange = + | "switch-active-tab" + | "edit-feature-title" + | "save-feature-props" + | "resize-component" + | "remove-component" + | "drag-drop-operation"; + +export type ApplicationLevelChange = + | "switch-active-layout" + | "open-layout" + | "close-layout" + | "rename-layout"; + +export type LayoutChangeReason = LayoutLevelChange | ApplicationLevelChange; + +export type LayoutChangeHandler = ( + layout: LayoutJSON, + layoutChangeReason: LayoutChangeReason +) => void; + +export const isApplicationLevelChange = ( + layoutChangeReason: LayoutChangeReason +): layoutChangeReason is ApplicationLevelChange => + [ + "switch-active-layout", + "open-layout", + "close-layout", + "rename-layout", + ].includes(layoutChangeReason); + +export const isLayoutLevelChange = ( + layoutChangeReason: LayoutChangeReason +): layoutChangeReason is LayoutLevelChange => + [ + "switch-active-tab", + "edit-feature-title", + "save-feature-props", + "remove-component", + "resize-component", + "drag-drop-operation", + ].includes(layoutChangeReason); diff --git a/vuu-ui/packages/vuu-layout/src/stack/StackLayout.tsx b/vuu-ui/packages/vuu-layout/src/stack/StackLayout.tsx index d7ff05581..a9a0e3107 100644 --- a/vuu-ui/packages/vuu-layout/src/stack/StackLayout.tsx +++ b/vuu-ui/packages/vuu-layout/src/stack/StackLayout.tsx @@ -49,7 +49,7 @@ export const StackLayout = (props: StackProps) => { const handleTabSelection = (nextIdx: number) => { if (path) { - dispatch({ type: "switch-tab", path, nextIdx }); + dispatch({ type: "switch-tab", id, path, nextIdx }); onTabSelectionChanged?.(nextIdx); } }; diff --git a/vuu-ui/packages/vuu-layout/src/utils/pathUtils.ts b/vuu-ui/packages/vuu-layout/src/utils/pathUtils.ts index c56c55fd3..34304ed6c 100644 --- a/vuu-ui/packages/vuu-layout/src/utils/pathUtils.ts +++ b/vuu-ui/packages/vuu-layout/src/utils/pathUtils.ts @@ -1,5 +1,5 @@ import React, { isValidElement, ReactElement } from "react"; -import { LayoutModel } from "../layout-reducer"; +import { LayoutJSON, LayoutModel, WithActive } from "../layout-reducer"; import { isContainer } from "../registry/ComponentRegistry"; import { getProp, getProps } from "./propUtils"; import { typeOf } from "./typeOf"; @@ -42,6 +42,30 @@ export const resolvePath = (source: ReactElement, path = ""): string => { return ""; }; +/** + * Similar to resolvePath but operates on a JSON + * layout structure and returns the matching JSON node. + */ +export const resolveJSONPath = ( + source: LayoutJSON, + path = "" +): LayoutJSON | undefined => { + const [step1, ...steps] = path.split("."); + if (step1?.startsWith("#")) { + const node = findTargetJSONById(source, step1.slice(1), true); + if (node && steps.length) { + return resolveJSONPath(node, steps.join(".")); + } + } else if (step1 === "ACTIVE_CHILD") { + const { children, props } = source; + const { active } = props as WithActive; + if (typeof active === "number" && children?.[active]) { + return children[active]; + } + } + return; +}; + export function followPathToParent( source: ReactElement, path: string @@ -146,6 +170,32 @@ const findTargetById = ( } }; +const findTargetJSONById = ( + source: LayoutJSON, + id: string, + throwIfNotFound = true +): LayoutJSON | undefined => { + const { children, id: idProp } = source; + if (idProp === id) { + return source; + } + + if (Array.isArray(children) && children.length > 0) { + for (const child of children) { + if (child !== null && typeof child === "object") { + const target = findTargetJSONById(child, id, false); + if (target) { + return target; + } + } + } + } + + if (throwIfNotFound === true) { + throw Error(`pathUtils.findTargetJSONById id #${id} not found in source`); + } +}; + export function followPath( source: LayoutModel, path: string diff --git a/vuu-ui/packages/vuu-shell/src/layout-config/local-config.ts b/vuu-ui/packages/vuu-shell/src/layout-config/local-config.ts index b8ad539a0..3655a7dae 100644 --- a/vuu-ui/packages/vuu-shell/src/layout-config/local-config.ts +++ b/vuu-ui/packages/vuu-shell/src/layout-config/local-config.ts @@ -1,4 +1,5 @@ import { LayoutJSON } from "@finos/vuu-layout/src/layout-reducer"; +import { resolveJSONPath } from "@finos/vuu-layout"; import { VuuUser } from "../shell"; export const loadLocalConfig = ( @@ -26,7 +27,10 @@ export const saveLocalConfig = ( ): Promise => new Promise((resolve, reject) => { try { - console.log(`save local config at ${saveUrl}`); + // Just for demonstration,not currently being used + const layoutJson = resolveJSONPath(data, "#main-tabs.ACTIVE_CHILD"); + console.log(layoutJson); + localStorage.setItem(saveUrl, JSON.stringify(data)); resolve(undefined); } catch { diff --git a/vuu-ui/packages/vuu-shell/src/shell.tsx b/vuu-ui/packages/vuu-shell/src/shell.tsx index 13834f787..ad69ef139 100644 --- a/vuu-ui/packages/vuu-shell/src/shell.tsx +++ b/vuu-ui/packages/vuu-shell/src/shell.tsx @@ -14,7 +14,10 @@ import { LayoutProvider, LayoutProviderProps, } from "@finos/vuu-layout"; -import { LayoutJSON } from "@finos/vuu-layout/src/layout-reducer"; +import { + LayoutChangeHandler, + LayoutJSON, +} from "@finos/vuu-layout/src/layout-reducer"; import { AppHeader } from "./app-header"; import { ThemeMode, ThemeProvider, useThemeAttributes } from "./theme-provider"; import { logger } from "@finos/vuu-utils"; @@ -85,9 +88,10 @@ export const Shell = ({ user, }); - const handleLayoutChange = useCallback( - (layout) => { + const handleLayoutChange = useCallback( + (layout, layoutChangeReason) => { try { + console.log(`handle layout changed ${layoutChangeReason}`); saveLayoutConfig(layout); } catch { error?.("Failed to save layout");