diff --git a/ui/package-lock.json b/ui/package-lock.json index 4cb74e29b8cd224d9a70949af33dbc9b68dd9b46..470921a86e7c92063df31669810c0324dc8c1b8c 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -23,8 +23,6 @@ "cypress-axe": "^1.5.0", "cypress-real-events": "^1.13.0", "dotenv": "^16.4.5", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", @@ -51,7 +49,6 @@ "@testing-library/jest-dom": "^6.2.0", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.2", - "@types/lodash": "^4.17.0", "@types/react": "^18.2.47", "@types/react-dom": "^18.2.18", "@types/sortablejs": "^1.15.8", @@ -5520,13 +5517,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/lodash": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", - "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/mdx": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", @@ -11199,12 +11189,6 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, - "node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", diff --git a/ui/package.json b/ui/package.json index cbbe1f64c67e4d6090b9c9769f4a1668f6fd6c90..2067a03e07a38a5a03eb8537f527b53cbf9c1c56 100644 --- a/ui/package.json +++ b/ui/package.json @@ -45,8 +45,6 @@ "cypress-axe": "^1.5.0", "cypress-real-events": "^1.13.0", "dotenv": "^16.4.5", - "lodash": "^4.17.21", - "lodash-es": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", @@ -73,7 +71,6 @@ "@testing-library/jest-dom": "^6.2.0", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.2", - "@types/lodash": "^4.17.0", "@types/react": "^18.2.47", "@types/react-dom": "^18.2.18", "@types/sortablejs": "^1.15.8", diff --git a/ui/src/components/form/inputBehaviors.tsx b/ui/src/components/form/inputBehaviors.tsx index edb177863d1491ed6c9738c69cc86391afc557fe..7375e29f4d82d13ad084b2c49bcf51601d76bcf2 100644 --- a/ui/src/components/form/inputBehaviors.tsx +++ b/ui/src/components/form/inputBehaviors.tsx @@ -1,5 +1,30 @@ import { useState, useContext, useEffect, useCallback } from 'react'; -import type * as React from 'react'; +import * as React from 'react'; +function useDebounce<F extends (...args: any[]) => void>(func: F, delay: number) { + const timeoutRef = React.useRef<NodeJS.Timeout | null>(null); + const savedFunc = React.useRef(func); + useEffect(() => { + savedFunc.current = func; + }, [func]); + + // Debounced function + const debouncedFunction = useCallback((...args: Parameters<F>) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => savedFunc.current(...args), delay); + }, [delay]); + const cancel = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }, []); + useEffect(() => { + return cancel; + }, [cancel]); + + return { debouncedFunction, cancel }; +} import { FormDispatchContext } from './components/drupal/DrupalForm'; import { selectSelectedComponent } from '@/features/ui/uiSlice'; import { useAppDispatch, useAppSelector } from '@/app/hooks'; @@ -8,7 +33,6 @@ import { selectModel, updateNodeModelForce, } from '@/features/layout/layoutModelSlice'; -import { debounce } from 'lodash'; import { useGetComponentsQuery } from '@/services/components'; import { findNodeByUuid } from '@/features/layout/layoutUtils'; import './InputBehaviors.css'; @@ -18,7 +42,6 @@ import { inputBehaviorOnChange, inputBehaviorOnBlur, } from '@/components/form/inputBehaviorsEventCallbacks'; - // Wraps all form elements to provide common functionality and subscribe to the // parent form's context. const InputBehaviors = (OriginalInput: React.FC) => { @@ -100,8 +123,7 @@ const InputBehaviors = (OriginalInput: React.FC) => { }, []); // Use debounce to prevent excessive repaints of the layout. - const debounceStoreUpdate = debounce(formStateToStore, 400); - + const { debouncedFunction: debounceStoreUpdate } = useDebounce(formStateToStore, 400); // Register the debounced store function as a callback so debouncing is // preserved between renders. const storeUpdateCallback = useCallback( diff --git a/ui/src/components/list/ComponentList.tsx b/ui/src/components/list/ComponentList.tsx index eb40cff78bc0be708d2a9f35677f36e752649621..849ef51665db1192ff4ca1ac007dc22a04b63127 100644 --- a/ui/src/components/list/ComponentList.tsx +++ b/ui/src/components/list/ComponentList.tsx @@ -2,7 +2,7 @@ import { useEffect } from 'react'; import { useErrorBoundary } from 'react-error-boundary'; import { useGetComponentsQuery } from '@/services/components'; import List from '@/components/list/List'; -import { toArray } from 'lodash'; +const toArray = (obj: any) => Object.values(obj); const ComponentList = () => { const { data: components, error, isLoading } = useGetComponentsQuery(); @@ -15,7 +15,7 @@ const ComponentList = () => { }, [error, showBoundary]); const sortedComponents = components - ? toArray(components).sort((a, b) => a.name.localeCompare(b.name)) + ? Object.values(components).sort((a, b) => a.name.localeCompare(b.name)) : {}; return ( diff --git a/ui/src/features/layout/layoutModelSlice.ts b/ui/src/features/layout/layoutModelSlice.ts index f9b2a93c33d2468d8335f408bf4496961c99a38b..932d77b1e00a0154a3a9a2439c1e2aba564797f3 100644 --- a/ui/src/features/layout/layoutModelSlice.ts +++ b/ui/src/features/layout/layoutModelSlice.ts @@ -1,11 +1,9 @@ -// cspell:ignore uuidv import type { AppDispatch } from '@/app/store'; import { setSelectedComponent } from '@/features/ui/uiSlice'; import type { Component } from '@/types/Component'; import type { UUID } from '@/types/UUID'; import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import _ from 'lodash'; import type { StateWithHistory } from 'redux-undo'; import { v4 as uuidv4 } from 'uuid'; import { @@ -17,6 +15,11 @@ import { removeNodeByUuid, replaceUUIDsAndUpdateModel, } from './layoutUtils'; +export interface ComponentModel { + [key: string]: string | boolean | number | object | any[]; +} + +export type ComponentModels = Record<string, ComponentModel>; export interface RootNode { name?: string; @@ -35,7 +38,6 @@ export interface Node { } export type LayoutNode = RootNode | Node; - export interface RootLayoutModel { layout: RootNode; model: ComponentModels; @@ -45,8 +47,6 @@ export interface LayoutModelSliceState extends RootLayoutModel { initialized: boolean; } -export type ComponentModels = Record<string, ComponentModel>; - export const initialState: LayoutModelSliceState = { layout: { uuid: 'root', @@ -58,60 +58,10 @@ export const initialState: LayoutModelSliceState = { initialized: false, }; -// This wrapper is necessary because when using slices with redux-undo, -// you reference state.[sliceName].present. export interface StateWithHistoryWrapper { layoutModel: StateWithHistory<LayoutModelSliceState>; } -type MoveNodePayload = { - uuid: string | undefined; - to: number[] | undefined; -}; - -type ShiftNodePayload = { - uuid: string | undefined; - direction: 'up' | 'down'; -}; - -type DuplicateNodePayload = { - uuid: string; -}; - -type InsertMultipleNodesPayload = { - to: number[] | undefined; - layoutModel: RootLayoutModel; - /** - * Pass an optional UUID that will be assigned to the last, top level node being inserted. Allows you to define the UUID - * so that you can then do something with the newly inserted node using that UUID. - */ - useUUID?: string; -}; - -type AddNewNodePayload = { - to: number[] | undefined; - component: Component | undefined; -}; - -type AddNewSectionPayload = { - to: number[] | undefined; - layoutModel: RootLayoutModel; -}; - -type SortNodePayload = { - uuid: string | undefined; - to: number | undefined; -}; - -type UpdateNodePayload = { - uuid: string | undefined; - model: {}; -}; - -export interface ComponentModel { - [key: string]: string | boolean | [] | number | {}; -} - export const layoutModelSlice = createSlice({ name: 'layoutModel', initialState, @@ -119,88 +69,58 @@ export const layoutModelSlice = createSlice({ deleteNode: create.reducer((state, action: PayloadAction<string>) => { const deletedComponent = findNodeByUuid(state.layout, action.payload); const removableModelsUuids = [action.payload]; + if (deletedComponent) { recurseNodes(deletedComponent, (node: LayoutNode) => { removableModelsUuids.push(node.uuid); }); } + for (const uuid of removableModelsUuids) { if (state.model[uuid]) delete state.model[uuid]; } + state.layout = removeNodeByUuid(state.layout, action.payload) as RootNode; }), - duplicateNode: create.reducer( - (state, action: PayloadAction<DuplicateNodePayload>) => { - const { uuid } = action.payload; - const nodeToDuplicate = findNodeByUuid(state.layout, uuid); - - if (!nodeToDuplicate) { - console.error(`Cannot duplicate ${uuid}. Check the uuid is valid.`); - return; - } - - const { updatedNode, updatedModel } = replaceUUIDsAndUpdateModel( - nodeToDuplicate, - state.model, - ); - // Add the updated model to the state - state.model = { ...state.model, ...updatedModel }; - - const nodePath = findNodePathByUuid(state.layout, uuid); - if (nodePath === null) { - console.error( - `Cannot find ${uuid} in layout. Check the uuid is valid.`, - ); - return; - } - nodePath[nodePath.length - 1]++; - state.layout = insertNodeAtPath( - state.layout, - nodePath, - updatedNode, - ) as RootNode; + setLayoutModel: create.reducer( + (state, action: PayloadAction<LayoutModelSliceState>) => { + const { layout, model, initialized } = action.payload; + state.layout = layout; + state.model = model; + state.initialized = initialized; }, ), - moveNode: create.reducer( - (state, action: PayloadAction<MoveNodePayload>) => { - const { uuid, to } = action.payload; - if (!uuid || !Array.isArray(to)) { - console.error( - `Cannot move ${uuid} to position ${to}. Check both uuid and to are defined/valid.`, - ); - return; - } - state.layout = moveNodeToPath(state.layout, uuid, to); + updateNodeModel: create.reducer( + (state, action: PayloadAction<{ uuid: string; model: {} }>) => { + const { uuid, model } = action.payload; + if (uuid) { + state.model[uuid] = { ...state.model[uuid], ...model }; + } }, ), + insertNodes: create.reducer( - (state, action: PayloadAction<InsertMultipleNodesPayload>) => { - const { layoutModel, to, useUUID } = action.payload; + (state, action: PayloadAction<{ to: number[]; layoutModel: RootLayoutModel }>) => { + const { layoutModel, to } = action.payload; if (!Array.isArray(to)) { - console.error( - `Cannot insert nodes. Invalid parameters: newNodes: ${layoutModel}, to: ${to}.`, - ); + console.error(`Cannot insert nodes.`); return; } let updatedModel: ComponentModels = { ...state.model }; - let newLayout: RootNode = _.cloneDeep(state.layout); + let newLayout: RootNode = structuredClone(state.layout); + const rootNode = layoutModel.layout; const model = layoutModel.model; - // Loop through each node in reverse order to maintain the correct insert positions for (let i = rootNode.children.length - 1; i >= 0; i--) { const node = rootNode.children[i]; - const specifyUUID = i === 0; const { updatedNode, updatedModel: nodeUpdatedModel } = - replaceUUIDsAndUpdateModel( - node, - model, - specifyUUID ? useUUID : undefined, - ); + replaceUUIDsAndUpdateModel(node, model); + updatedModel = { ...updatedModel, ...nodeUpdatedModel }; newLayout = insertNodeAtPath(newLayout, to, updatedNode); } @@ -209,17 +129,16 @@ export const layoutModelSlice = createSlice({ state.layout = newLayout; }, ), + sortNode: create.reducer( - (state, action: PayloadAction<SortNodePayload>) => { + (state, action: PayloadAction<{ uuid: string; to: number }>) => { const { uuid, to } = action.payload; if (!uuid || to === undefined) { - console.error( - `Cannot sort ${uuid} to position ${to}. Check both uuid and to are defined/valid.`, - ); + console.error(`Cannot sort ${uuid}.`); return; } - const cloneNode = _.cloneDeep(findNodeByUuid(state.layout, uuid)); + const cloneNode = structuredClone(findNodeByUuid(state.layout, uuid)); const nodePath = findNodePathByUuid(state.layout, uuid); if (cloneNode && nodePath) { const insertPosition = [...nodePath.slice(0, -1), to]; @@ -229,17 +148,16 @@ export const layoutModelSlice = createSlice({ } }, ), + shiftNode: create.reducer( - (state, action: PayloadAction<ShiftNodePayload>) => { + (state, action: PayloadAction<{ uuid: string; direction: 'up' | 'down' }>) => { const { uuid, direction } = action.payload; if (!uuid) { - console.error( - `Cannot shift ${uuid} ${direction}. Check both uuid and direction are defined/valid.`, - ); + console.error(`Cannot shift ${uuid} ${direction}.`); return; } - const cloneNode = _.cloneDeep(findNodeByUuid(state.layout, uuid)); + const cloneNode = structuredClone(findNodeByUuid(state.layout, uuid)); const nodePath = findNodePathByUuid(state.layout, uuid); if (cloneNode && nodePath) { const newPos = @@ -253,140 +171,23 @@ export const layoutModelSlice = createSlice({ } }, ), - setLayoutModel: create.reducer( - (state, action: PayloadAction<LayoutModelSliceState>) => { - const { layout, model, initialized } = action.payload; - state.layout = layout; - state.model = model; - state.initialized = initialized; - }, - ), - updateNodeModel: create.reducer( - (state, action: PayloadAction<UpdateNodePayload>) => { - const { uuid, model } = action.payload; - const randomData = { randomProp: 'random' }; - if (uuid) { - state.model[uuid] = { ...state.model[uuid], ...model, ...randomData }; - } - }, - ), - // Nearly identical to updateNodeModel above, but makes it possible to - // remove props by not including the prior state.model[uuid] in the value - // update. - updateNodeModelForce: create.reducer( - (state, action: PayloadAction<UpdateNodePayload>) => { - const { uuid, model } = action.payload; - if (uuid) { - state.model[uuid] = { ...(model as ComponentModel) }; - } - }, - ), }), }); -export const addNewComponentToLayout = - (payload: AddNewNodePayload) => (dispatch: AppDispatch) => { - if (!payload.to || !payload.component) { - return; - } - - const initialData: ComponentModel = {}; - const children: Node[] = []; - const uuid = uuidv4(); - - // Populate the model data with the default values - if (payload.component?.field_data) { - Object.keys(payload.component.field_data).forEach((propName) => { - if (payload.component?.field_data?.[propName]?.['default_values']) { - initialData[propName] = - payload.component?.field_data[propName]['default_values']; - } - }); - } - - // Create empty slots in the layout data for each child slot the component has - if (payload.component?.metadata?.slots) { - Object.keys(payload.component.metadata.slots).forEach((name) => { - children.push({ - uuid: `-slot-${name}`, - name: name, - nodeType: 'slot', - children: [], - }); - }); - } - - const layoutModel: RootLayoutModel = { - layout: { - children: [ - { - children, - nodeType: 'component', - type: payload.component.id, - uuid: uuid, - }, - ], - nodeType: 'root', - uuid: 'root', - }, - model: { - [uuid]: initialData, - }, - }; - - dispatch( - insertNodes({ - to: payload.to, - layoutModel, - useUUID: uuid, - }), - ); - dispatch(setSelectedComponent(uuid)); - }; - -export const addNewSectionToLayout = - (payload: AddNewSectionPayload) => (dispatch: AppDispatch) => { - const uuid = uuidv4(); - - const { to, layoutModel } = payload; - - if (!to || !layoutModel) { - return; - } - - dispatch( - insertNodes({ - to, - layoutModel, - useUUID: uuid, - }), - ); - dispatch(setSelectedComponent(uuid)); - }; - -// Action creators are generated for each case reducer function. export const { deleteNode, setLayoutModel, - duplicateNode, - moveNode, - shiftNode, - sortNode, - insertNodes, updateNodeModel, - updateNodeModelForce, + insertNodes, + sortNode, + shiftNode, } = layoutModelSlice.actions; export const layoutModelReducer = layoutModelSlice.reducer; -// When using redux-undo, you reference the current state by state.[sliceName].present.[targetKey]. -// These selectors are written outside the slice because the type of state is different. Here, we need -// to be able to access the history, so we use the StateWithHistoryWrapper type. export const selectLayout = (state: StateWithHistoryWrapper) => state.layoutModel.present.layout; export const selectModel = (state: StateWithHistoryWrapper) => state.layoutModel.present.model; -export const selectHistory = (state: StateWithHistoryWrapper) => - state.layoutModel; export const selectInitialized = (state: StateWithHistoryWrapper) => state.layoutModel.present.initialized; diff --git a/ui/src/features/layout/layoutUtils.ts b/ui/src/features/layout/layoutUtils.ts index 250f7742966cd0c1f9c20e0979265d97e6b222f5..9fe5675ab2e69c59f1f93c033ea93d962a3a6d22 100644 --- a/ui/src/features/layout/layoutUtils.ts +++ b/ui/src/features/layout/layoutUtils.ts @@ -1,4 +1,3 @@ -import _ from 'lodash'; import type { ComponentModels, LayoutNode, @@ -13,20 +12,16 @@ type NodeFunction = ( parent: LayoutNode, ) => void; -/** - * Recursively run one or multiple functions against a node and all its descendants. - * @param node - A layout or layout node. - * @param functionOrFunctions - A function or an array of functions to run on a node and all of its child nodes. - * Each function is passed 3 parameters: the node, its index, and its direct parent. - */ export function recurseNodes( node: LayoutNode | LayoutNode[], functionOrFunctions: NodeFunction | NodeFunction[] = [], ): void { - let functionsToRun: NodeFunction[] = _.castArray(functionOrFunctions); - let children: LayoutNode[] = Array.isArray(node) ? node : node.children || []; + const functionsToRun: NodeFunction[] = Array.isArray(functionOrFunctions) + ? functionOrFunctions + : [functionOrFunctions]; + + const children: LayoutNode[] = Array.isArray(node) ? node : node.children || []; - // Loop backwards in case the array is modified by the passed function/functions for (let index = children.length - 1; index >= 0; index--) { const child = children[index]; @@ -42,46 +37,29 @@ export function recurseNodes( } } -/** - * Find a node by its UUID. - * @param node - The starting node to search from. - * @param uuid - The UUID of the node to find. - * @returns The found node or null if not found. - */ export function findNodeByUuid(node: LayoutNode, uuid: string): Node | null { if (node.uuid === uuid) { - if (node.nodeType === 'root') { - return null; - } - return node; + return node.nodeType === 'root' ? null : node; } if (node.children) { for (const child of node.children) { const result = findNodeByUuid(child, uuid); - if (result) { - return result; - } + if (result) return result; } } return null; } -/** - * Find the path to a node by its UUID. - * @param node - The starting node to search from. - * @param uuid - The UUID of the node to find. - * @param path - The current path (used internally for recursion). - * @returns The path to the node as an array of indices, or null if not found. - */ export function findNodePathByUuid( node: LayoutNode, uuid: string | undefined, path: number[] = [], ): number[] | null { if (!uuid) { - console.error('No uuid provided to findNodePathByUuid.'); + console.error('No uuid provided.'); return null; } + if (node.uuid === uuid) { return path; } @@ -89,96 +67,52 @@ export function findNodePathByUuid( if (node.children) { for (let i = 0; i < node.children.length; i++) { const child = node.children[i]; - // Recursively search in the child node, appending the current index to the path const result = findNodePathByUuid(child, uuid, path.concat(i)); - // If the result is not null, the node has been found in the subtree - if (result !== null) { - return result; - } + if (result !== null) return result; } } - // If the node is not found in this subtree, return null return null; } -/** - * Remove a node by its UUID. - * @param node - The starting node to search from. - * @param uuid - The UUID of the node to remove. - * @returns A deep clone of the node with the node matching the uuid removed. - */ export function removeNodeByUuid<T extends LayoutNode>( node: T, uuid: string, ): T { - const newState = _.cloneDeep(node); + const newState = structuredClone(node); const path = findNodePathByUuid(newState, uuid); if (path) { - const lodashPath = path.map((index) => `children[${index}]`).join('.'); - const parentPath = lodashPath.split('.').slice(0, -1).join('.'); - const i = path[path.length - 1]; - const parent = parentPath ? _.get(newState, parentPath) : newState; - if (parent && parent.children) { - parent.children.splice(i, 1); + let parent = newState as LayoutNode; + for (let i = 0; i < path.length - 1; i++) { + parent = parent.children[path[i]]; } + parent.children.splice(path[path.length - 1], 1); } return newState; } -/** - * Insert a node at a specific path. - * @param layoutNode - The starting node to insert into. - * @param path - The path where the new node should be inserted. - * @param newNode - The new node to insert. - * @returns A deep clone of the node with the newNode inserted at path. - */ export function insertNodeAtPath<T extends LayoutNode>( layoutNode: T, path: number[], newNode: Node, ): T { - const newState = _.cloneDeep(layoutNode); + const newState = structuredClone(layoutNode); if (path.length === 0) { - throw new Error( - 'Path must have at least one element to define where to insert the node.', - ); + throw new Error('Path must have at least one element.'); } - // Base case: if the path has only one element, insert the new node at the specified index - if (path.length === 1) { - newState.children = newState.children || []; - newState.children.splice(path[0], 0, newNode); - return newState; + let parent = newState as LayoutNode; + for (let i = 0; i < path.length - 1; i++) { + parent = parent.children[path[i]]; } - // Recursive case: navigate down the path - const [currentIndex, ...restOfPath] = path; - newState.children = newState.children || []; - if (!newState.children[currentIndex]) { - throw new Error('Path must resolve to a node in the tree.'); - } - - // Recursively insert the node at the remaining path and update the child node - newState.children[currentIndex] = insertNodeAtPath( - newState.children[currentIndex], - restOfPath, - newNode, - ) as Node; - + parent.children.splice(path[path.length - 1], 0, newNode); return newState; } -/** - * Move a node to a new path. - * @param rootNode - The root node of the layout. - * @param uuid - The UUID of the node to move. - * @param path - The path to move the node to. - * @returns A deep clone of the `rootNode` with the node matching the `uuid` moved to the `path`. - */ export function moveNodeToPath( rootNode: RootNode, uuid: string, @@ -188,54 +122,14 @@ export function moveNodeToPath( if (!child) { throw new Error(`Node with UUID ${uuid} not found.`); } - // Make a clone of the node that is being moved. - const clone = _.cloneDeep(child); - // flag the original node for deletion - child.uuid = child.uuid + '_remove'; - // Insert the clone at toPath - const newState = insertNodeAtPath(rootNode, path, clone); + const clone = structuredClone(child); + child.uuid += '_remove'; - // Remove the original node by finding it by uuid (which is now `${child.uuid}_remove`) + const newState = insertNodeAtPath(rootNode, path, clone); return removeNodeByUuid(newState, child.uuid); } -/** - * Checks if a node is a child of another node. - * @param layoutNode - The root node. - * @param uuid - The UUID of the node to check. - * @returns {boolean | null} - Returns if node is a child or not and null if the node is not found. - */ -export function isChildNode(layoutNode: LayoutNode, uuid: string) { - const path = findNodePathByUuid(layoutNode, uuid); - if (path !== null) { - return path && path.length > 1; - } else { - return null; - } -} - -/** - * Get the depth of the node in the layout tree from the root. - * @param layoutNode - The root node. - * @param uuid - The UUID of the node to check. - * @returns Depth of a node as an integer. - */ -export function getNodeDepth(layoutNode: LayoutNode, uuid: string | undefined) { - const path = findNodePathByUuid(layoutNode, uuid); - if (path) { - return path.length - 1; - } - return 0; -} - -/** - * Replace UUIDs in a layout node and its corresponding model. - * @param node - The layout node to update. - * @param model - The corresponding model to update. - * @param newUUID - Optionally specify the UUID of the new node and its model. - * @returns An updated model and an updated state. - */ export function replaceUUIDsAndUpdateModel( node: Node, model: ComponentModels, @@ -247,19 +141,15 @@ export function replaceUUIDsAndUpdateModel( const oldToNewUUIDMap: Record<string, string> = {}; const updatedModel: ComponentModels = {}; - const replaceUUIDs = ( - node: Node, - parentUuid?: string, - newUuid?: string, - ): Node => { + const replaceUUIDs = (node: Node, parentUuid?: string, newUuid?: string): Node => { const newNode: Node = { ...node, uuid: newUuid || uuidv4() }; + if (newNode.nodeType === 'slot') { newNode.uuid = `${parentUuid}-slot-${newNode.name}`; } oldToNewUUIDMap[node.uuid] = newNode.uuid; - // Recursively process children if (newNode.children) { newNode.children = newNode.children.map((child) => replaceUUIDs(child, newNode.uuid), @@ -271,13 +161,12 @@ export function replaceUUIDsAndUpdateModel( const updatedNode = replaceUUIDs(node, undefined, newUUID); - // Update the model keys - for (const oldUUID in model) { + Object.keys(model).forEach((oldUUID) => { const newUUID = oldToNewUUIDMap[oldUUID]; if (newUUID) { - updatedModel[newUUID] = _.cloneDeep(model[oldUUID]); + updatedModel[newUUID] = structuredClone(model[oldUUID]); } - } + }); return { updatedNode, updatedModel }; } diff --git a/ui/src/local_packages/hyperscriptify/propsify/standard/index.js b/ui/src/local_packages/hyperscriptify/propsify/standard/index.js index 939fda9743689fe501afa14991104f677a7e4451..01de9b0c5351ce155601ae61aa36e917f616c5eb 100644 --- a/ui/src/local_packages/hyperscriptify/propsify/standard/index.js +++ b/ui/src/local_packages/hyperscriptify/propsify/standard/index.js @@ -1,5 +1,17 @@ import factory from '../factory'; -import { camelCase, mapKeys, mapValues, set } from 'lodash-es'; +const camelCase = (str) => str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); +const mapKeys = (obj, fn) => + Object.fromEntries(Object.entries(obj).map(([k, v]) => [fn(k), v])); +const mapValues = (obj, fn) => + Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)])); +const set = (obj, path, value) => { + const keys = path.split('.'); + let current = obj; + for (let i = 0; i < keys.length - 1; i++) { + current = current[keys[i]] = current[keys[i]] || {}; + } + current[keys[keys.length - 1]] = value; +}; import htmlElementAttributeToPropMap from './reactHtmlAttributeToPropertyMap'; const basePropsify = factory({