From 92a0d607a99566c25fe4ee17b1a77509f2320b45 Mon Sep 17 00:00:00 2001 From: Taya Leutina Date: Mon, 20 Oct 2025 15:32:42 +0300 Subject: [PATCH 1/5] feat: support globalItems in config --- .../DashKit/__stories__/DashKit.stories.tsx | 8 +- .../DashKit/__stories__/DashKitShowcase.tsx | 3 +- src/components/DashKit/__stories__/utils.ts | 18 + src/components/GridLayout/GridLayout.js | 8 +- src/components/MobileLayout/MobileLayout.tsx | 16 +- src/components/MobileLayout/helpers.ts | 8 +- src/context/DashKitContext.ts | 1 + src/hocs/withContext.js | 14 +- src/hooks/useCalcLayout.ts | 4 +- .../modules/__tests__/global-items.test.ts | 357 ++++++++++++++++++ src/shared/modules/helpers.ts | 7 +- src/shared/modules/state-and-params.ts | 9 +- src/shared/modules/uniq-id.ts | 2 +- src/shared/types/config.ts | 1 + src/typings/config.ts | 1 + src/utils/update-manager.ts | 98 +++-- 16 files changed, 510 insertions(+), 45 deletions(-) create mode 100644 src/shared/modules/__tests__/global-items.test.ts diff --git a/src/components/DashKit/__stories__/DashKit.stories.tsx b/src/components/DashKit/__stories__/DashKit.stories.tsx index d2bbad0..35a2abe 100644 --- a/src/components/DashKit/__stories__/DashKit.stories.tsx +++ b/src/components/DashKit/__stories__/DashKit.stories.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {Meta, Story} from '@storybook/react'; -import pluginText from '../../../plugins/Text/Text'; +import pluginText, {PluginTextProps} from '../../../plugins/Text/Text'; import pluginTitle from '../../../plugins/Title/Title'; import {cn} from '../../../utils/cn'; import {DashKit, DashKitProps} from '../DashKit'; @@ -54,11 +54,13 @@ if (!getInitialized()) { w: 20, h: 20, }, - renderer: function CustomPlugin() { + renderer: function CustomPlugin(props: PluginTextProps) { return (
-
Custom widget
+
+ {props.data.text ?? 'Custom widget'} +
); diff --git a/src/components/DashKit/__stories__/DashKitShowcase.tsx b/src/components/DashKit/__stories__/DashKitShowcase.tsx index 7a525f2..bd4f13b 100644 --- a/src/components/DashKit/__stories__/DashKitShowcase.tsx +++ b/src/components/DashKit/__stories__/DashKitShowcase.tsx @@ -420,7 +420,8 @@ export class DashKitShowcase extends React.Component<{}, DashKitDemoState> { }; private isTitleInConfig() { - return Boolean(this.state.config.items.find((item) => item.id === titleId)); + const allItems = [...this.state.config.items, ...(this.state.config.globalItems || [])]; + return Boolean(allItems.find((item) => item.id === titleId)); } private toggleOverlayControls = () => { diff --git a/src/components/DashKit/__stories__/utils.ts b/src/components/DashKit/__stories__/utils.ts index 7c9dc6a..aedb0c8 100644 --- a/src/components/DashKit/__stories__/utils.ts +++ b/src/components/DashKit/__stories__/utils.ts @@ -83,6 +83,17 @@ export const getConfig = (withGroups?: boolean): DashKitProps['config'] => ({ ] : []), ], + globalItems: [ + { + id: 'mJ', + data: { + text: 'Global item', + }, + type: 'custom', + namespace: 'default', + orderId: 5, + }, + ], layout: [ { h: 2, @@ -112,6 +123,13 @@ export const getConfig = (withGroups?: boolean): DashKitProps['config'] => ({ x: 0, y: 8, }, + { + h: 10, + i: 'mJ', + w: 10, + x: 10, + y: 8, + }, ...(withGroups ? [ { diff --git a/src/components/GridLayout/GridLayout.js b/src/components/GridLayout/GridLayout.js index c0c5cd0..e6d43b2 100644 --- a/src/components/GridLayout/GridLayout.js +++ b/src/components/GridLayout/GridLayout.js @@ -260,7 +260,7 @@ export default class GridLayout extends React.PureComponent { const item = temporaryLayout ? temporaryLayout.dragProps - : this.context.config.items.find(({id}) => id === layoutId); + : this.context.configItems.find(({id}) => id === layoutId); let {offsetX, offsetY} = e.nativeEvent || {}; if (offsetX === undefined || offsetY === undefined) { @@ -697,7 +697,9 @@ export default class GridLayout extends React.PureComponent { render() { const {config, groups, editMode, context} = this.context; - this.pluginsRefs.length = config.items.length; + const configItems = this.context.configItems; + + this.pluginsRefs.length = configItems.length; const defaultRenderLayout = []; const defaultRenderItems = []; @@ -718,7 +720,7 @@ export default class GridLayout extends React.PureComponent { return memo; }, []); - const itemsByGroup = config.items.reduce((memo, item) => { + const itemsByGroup = configItems.reduce((memo, item) => { const group = layoutMap[item.id]; if (group) { if (!memo[group]) { diff --git a/src/components/MobileLayout/MobileLayout.tsx b/src/components/MobileLayout/MobileLayout.tsx index 12b75a5..613b7cf 100644 --- a/src/components/MobileLayout/MobileLayout.tsx +++ b/src/components/MobileLayout/MobileLayout.tsx @@ -40,9 +40,16 @@ export default class MobileLayout extends React.PureComponent< }; render() { - const {config, layout, groups = [{id: DEFAULT_GROUP}], context, editMode} = this.context; + const { + config, + layout, + groups = [{id: DEFAULT_GROUP}], + context, + editMode, + configItems, + } = this.context; - this.pluginsRefs.length = config.items.length; + this.pluginsRefs.length = configItems.length; const sortedItems = this.getSortedLayoutItems(); let indexOffset = 0; @@ -103,11 +110,12 @@ export default class MobileLayout extends React.PureComponent< } this._memoLayout = this.context.layout; + const configItems = this.context.configItems; - const hasOrderId = Boolean(this.context.config.items.find((item) => item.orderId)); + const hasOrderId = Boolean(configItems.find((item) => item.orderId)); this.sortedLayoutItems = groupBy( - getSortedConfigItems(this.context.config, hasOrderId), + getSortedConfigItems(this.context.config, configItems, hasOrderId), (item) => item.parent || DEFAULT_GROUP, ); diff --git a/src/components/MobileLayout/helpers.ts b/src/components/MobileLayout/helpers.ts index 8d63e90..388c0cd 100644 --- a/src/components/MobileLayout/helpers.ts +++ b/src/components/MobileLayout/helpers.ts @@ -26,10 +26,14 @@ const getWidgetsSortComparator = (hasOrderId: boolean) => { prev.y === next.y ? prev.x - next.x : prev.y - next.y; }; -export const getSortedConfigItems = (config: DashKitProps['config'], hasOrderId: boolean) => { +export const getSortedConfigItems = ( + config: DashKitProps['config'], + configItems: ConfigItem[], + hasOrderId: boolean, +) => { const sortComparator = getWidgetsSortComparator(hasOrderId); - return config.items + return configItems .map((item, index) => Object.assign({}, item, config.layout[index])) .sort(sortComparator); }; diff --git a/src/context/DashKitContext.ts b/src/context/DashKitContext.ts index 172322e..53d35be 100644 --- a/src/context/DashKitContext.ts +++ b/src/context/DashKitContext.ts @@ -39,6 +39,7 @@ export type DashKitCtxShape = DashkitPropsPassedToCtx & { registerManager: RegisterManager; forwardedMetaRef: React.ForwardedRef; + configItems: ConfigItem[]; layout: ConfigLayout[]; temporaryLayout: ConfigLayout[] | null; memorizeOriginalLayout: ( diff --git a/src/hocs/withContext.js b/src/hocs/withContext.js index ac47e5b..8793dcb 100644 --- a/src/hocs/withContext.js +++ b/src/hocs/withContext.js @@ -104,6 +104,11 @@ function useMemoStateContext(props) { [props.config.layout], ); + const configItems = React.useMemo( + () => props.config.items.concat(props.config.globalItems || []), + [props.config.items, props.config.globalItems], + ); + const onItemRemove = React.useCallback( (id) => { delete nowrapAdjustedLayouts.current[id]; @@ -130,12 +135,13 @@ function useMemoStateContext(props) { } }, [ - props.config, - props.itemsStateAndParams, + resetTemporaryLayout, temporaryLayout, onChange, + props.config, + props.itemsStateAndParams, + configItems, setTemporaryLayout, - resetTemporaryLayout, ], ); @@ -433,6 +439,7 @@ function useMemoStateContext(props) { const dashkitContextValue = React.useMemo( () => ({ config: props.config, + configItems, groups: props.groups, context: props.context, noOverlay: props.noOverlay, @@ -481,6 +488,7 @@ function useMemoStateContext(props) { resultLayout, temporaryLayout, props.config, + configItems, props.groups, props.context, props.noOverlay, diff --git a/src/hooks/useCalcLayout.ts b/src/hooks/useCalcLayout.ts index d81a384..7358a69 100644 --- a/src/hooks/useCalcLayout.ts +++ b/src/hooks/useCalcLayout.ts @@ -6,8 +6,9 @@ import type {Config} from '../shared'; import {RegisterManager} from '../utils'; function onUpdatePropsConfig(config: Config, registerManager: RegisterManager) { + const configItems = [...config.items, ...(config.globalItems || [])]; return config.layout.map((itemLayout, i) => { - const {type} = config.items[i]; + const {type} = configItems[i]; return { ...registerManager.getItem(type).defaultLayout, ...itemLayout, @@ -17,6 +18,7 @@ function onUpdatePropsConfig(config: Config, registerManager: RegisterManager) { export const useCalcPropsLayout = (config: Config, registerManager: RegisterManager) => { const [prevConfig, setPrevConfig] = React.useState(config); + const [layout, updateLayout] = React.useState(onUpdatePropsConfig(config, registerManager)); if (!isEqual(prevConfig.layout, config.layout)) { diff --git a/src/shared/modules/__tests__/global-items.test.ts b/src/shared/modules/__tests__/global-items.test.ts new file mode 100644 index 0000000..fcc6b56 --- /dev/null +++ b/src/shared/modules/__tests__/global-items.test.ts @@ -0,0 +1,357 @@ +import {META_KEY} from '../../constants'; +import { + Config, + ConfigItem, + ItemsStateAndParams, + ItemsStateAndParamsBase, + StringParams, +} from '../../types'; +import {addGroupToQueue, addToQueue, formQueueData} from '../helpers'; +import {getItemsParams, getItemsStateAndParams} from '../state-and-params'; + +const NAMESPACE = 'default'; +const GLOBAL_ITEM_ID = 'globalItem1'; +const REGULAR_ITEM_ID = 'regularItem1'; +const GROUP_ITEM_ID = 'groupItem1'; + +const getMockedGlobalItem = (id: string = GLOBAL_ITEM_ID, defaults?: StringParams): ConfigItem => ({ + id, + defaults, + data: {}, + type: 'control', + namespace: NAMESPACE, +}); + +const getMockedRegularItem = ( + id: string = REGULAR_ITEM_ID, + defaults?: StringParams, +): ConfigItem => ({ + id, + defaults, + data: {}, + type: 'control', + namespace: NAMESPACE, +}); + +const getMockedGroupItem = ( + id: string, + groupItemIds: string[] = [GROUP_ITEM_ID], + defaults?: StringParams, +): ConfigItem => ({ + id, + data: { + group: groupItemIds.map((groupItemId) => ({ + id: groupItemId, + namespace: NAMESPACE, + defaults, + })), + }, + type: 'group-control', + namespace: NAMESPACE, +}); + +const getMockedConfig = ({ + items = [], + globalItems = [], +}: { + items?: ConfigItem[]; + globalItems?: ConfigItem[]; +}): Config => ({ + items, + globalItems, + salt: '0.9021043992843898', + counter: 124, + layout: [], + aliases: {}, + connections: [], +}); + +describe('globalItems functionality in config', () => { + describe('addToQueue with globalItems', () => { + it('should include globalItems when filtering actual IDs', () => { + const globalItem = getMockedGlobalItem(); + const regularItem = getMockedRegularItem(); + const config = getMockedConfig({ + items: [regularItem], + globalItems: [globalItem], + }); + + const itemsStateAndParams: ItemsStateAndParams = { + [META_KEY]: { + queue: [{id: 'nonExistentItem'}, {id: GLOBAL_ITEM_ID}], + version: 2, + }, + }; + + const result = addToQueue({ + id: REGULAR_ITEM_ID, + config, + itemsStateAndParams, + }); + + // Should filter out non-existent items and add the new item + expect(result.queue).toEqual([{id: GLOBAL_ITEM_ID}, {id: REGULAR_ITEM_ID}]); + }); + + it('should handle empty globalItems array', () => { + const regularItem = getMockedRegularItem(); + const config = getMockedConfig({ + items: [regularItem], + globalItems: [], + }); + + const itemsStateAndParams: ItemsStateAndParams = { + [META_KEY]: { + queue: [], + version: 2, + }, + }; + + const result = addToQueue({ + id: REGULAR_ITEM_ID, + config, + itemsStateAndParams, + }); + + expect(result.queue).toEqual([{id: REGULAR_ITEM_ID}]); + }); + }); + + describe('addGroupToQueue with globalItems', () => { + it('should include globalItems when filtering actual IDs for group items', () => { + const globalGroupItemId = 'globalGroupItem'; + const globalGroupSubItemId = 'globalGroupSubItem'; + const globalGroupSubItemId2 = 'globalGroupSubItem2'; + const globalGroupItem = getMockedGroupItem(globalGroupItemId, [ + globalGroupSubItemId, + globalGroupSubItemId2, + ]); + const regularItem = getMockedRegularItem(); + const config = getMockedConfig({ + items: [regularItem], + globalItems: [globalGroupItem], + }); + + const itemsStateAndParams: ItemsStateAndParams = { + [META_KEY]: { + queue: [ + {id: globalGroupItemId, groupItemId: globalGroupSubItemId}, + {id: 'nonExistentGroup', groupItemId: 'nonExistentSubItem'}, + ], + version: 2, + }, + }; + + const result = addGroupToQueue({ + id: globalGroupItemId, + groupItemIds: [globalGroupSubItemId2], + config, + itemsStateAndParams, + }); + + // Should preserve existing global group item and add new one + expect(result.queue).toEqual([ + {id: globalGroupItemId, groupItemId: globalGroupSubItemId}, + {id: globalGroupItemId, groupItemId: globalGroupSubItemId2}, + ]); + }); + }); + + describe('formQueueData with globalItems', () => { + it('should process globalItems in queue data formation', () => { + const globalItem = getMockedGroupItem(GLOBAL_ITEM_ID, [GROUP_ITEM_ID], { + size: 'l', + }); + const regularItem = getMockedRegularItem(REGULAR_ITEM_ID, {view: 'normal'}); + + const itemsStateAndParams: ItemsStateAndParams = { + [GLOBAL_ITEM_ID]: { + params: { + [GROUP_ITEM_ID]: { + size: 'xl', + color: 'red', + }, + }, + }, + [REGULAR_ITEM_ID]: { + params: { + view: 'contrast', + }, + }, + [META_KEY]: { + queue: [ + {id: GLOBAL_ITEM_ID, groupItemId: GROUP_ITEM_ID}, + {id: REGULAR_ITEM_ID}, + ], + version: 2, + }, + }; + + const result = formQueueData({ + items: [regularItem, globalItem], + itemsStateAndParams, + }); + + expect(result).toEqual([ + { + id: GROUP_ITEM_ID, + namespace: NAMESPACE, + params: {size: 'xl'}, + }, + { + id: REGULAR_ITEM_ID, + namespace: NAMESPACE, + params: {view: 'contrast'}, + }, + ]); + }); + }); + + describe('getItemsParams with globalItems', () => { + it('should process globalItems when getting items parameters', () => { + const globalParam = 'globalParam'; + const overriddenParam = 'overriddenValue'; + const regularParam = 'regularValue'; + const globalItem = getMockedGlobalItem(GLOBAL_ITEM_ID, {globalParam}); + const regularItem = getMockedRegularItem(REGULAR_ITEM_ID, { + regularParam, + }); + + const config = getMockedConfig({ + items: [regularItem], + globalItems: [globalItem], + }); + + const itemsStateAndParams: ItemsStateAndParams = { + [GLOBAL_ITEM_ID]: { + params: {globalParam: overriddenParam}, + }, + [META_KEY]: { + queue: [{id: GLOBAL_ITEM_ID}], + version: 2, + }, + }; + + const result = getItemsParams({ + defaultGlobalParams: {}, + globalParams: {}, + config, + itemsStateAndParams, + plugins: [ + { + type: 'control', + }, + ], + }); + + expect(result[GLOBAL_ITEM_ID]).toEqual({ + globalParam: overriddenParam, + regularParam, + }); + expect(result[REGULAR_ITEM_ID]).toEqual({ + globalParam: overriddenParam, + regularParam, + }); + }); + }); + + describe('getItemsStateAndParams with globalItems', () => { + it('should handle globalItems in state and params processing', () => { + const initialValue = 'value'; + const globalParam = 'globalValue'; + const regularParam = 'regularValue'; + const globalState = 'globalStateValue'; + const regularState = 'regularStateValue'; + const globalItem = getMockedGlobalItem(GLOBAL_ITEM_ID, {globalDefault: initialValue}); + const regularItem = getMockedRegularItem(REGULAR_ITEM_ID, { + regularDefault: initialValue, + }); + + const config = getMockedConfig({ + items: [regularItem], + globalItems: [globalItem], + }); + + // globalParam and regularParam are not included in conrols defaults so they must be ignored + const itemsStateAndParams: ItemsStateAndParams = { + [GLOBAL_ITEM_ID]: { + params: {globalParam}, + state: {globalState}, + }, + [REGULAR_ITEM_ID]: { + params: {regularParam}, + state: {regularState}, + }, + [META_KEY]: { + queue: [{id: GLOBAL_ITEM_ID}, {id: REGULAR_ITEM_ID}], + version: 2, + }, + }; + + const result = getItemsStateAndParams({ + defaultGlobalParams: {}, + globalParams: {}, + config, + itemsStateAndParams, + plugins: [ + { + type: 'control', + }, + { + type: 'group-control', + }, + ], + }) as ItemsStateAndParamsBase; + + expect(result[GLOBAL_ITEM_ID]).toEqual({ + params: { + globalDefault: initialValue, + regularDefault: initialValue, + }, + state: {globalState}, + }); + + expect(result[REGULAR_ITEM_ID]).toEqual({ + params: { + globalDefault: initialValue, + regularDefault: initialValue, + }, + state: {regularState}, + }); + + expect(result[META_KEY]).toEqual({ + queue: [{id: GLOBAL_ITEM_ID}, {id: REGULAR_ITEM_ID}], + version: 2, + }); + }); + }); + + describe('edge cases with globalItems', () => { + it('should handle config with only globalItems and no regular items', () => { + const globalItemId1 = 'global1'; + const globalItemId2 = 'global2'; + const globalItem1 = getMockedGlobalItem(globalItemId1); + const globalItem2 = getMockedGlobalItem(globalItemId2); + + const config = getMockedConfig({ + items: [], + globalItems: [globalItem1, globalItem2], + }); + + const itemsStateAndParams: ItemsStateAndParams = { + [META_KEY]: { + queue: [{id: globalItemId1}], + version: 2, + }, + }; + + const result = addToQueue({ + id: globalItemId2, + config, + itemsStateAndParams, + }); + + expect(result.queue).toEqual([{id: globalItemId1}, {id: globalItemId2}]); + }); + }); +}); diff --git a/src/shared/modules/helpers.ts b/src/shared/modules/helpers.ts index a35c3fc..bcc65c4 100644 --- a/src/shared/modules/helpers.ts +++ b/src/shared/modules/helpers.ts @@ -319,7 +319,9 @@ export function addToQueue({ if (!meta) { return {queue: [queueItem], version: CURRENT_VERSION}; } - const actualIds = getActualItemsIds(config.items); + + const configItems = config.items.concat(config.globalItems || []); + const actualIds = getActualItemsIds(configItems); const metaQueue = meta.queue || []; const notCurrent = (item: QueueItem) => { if (item.groupItemId) { @@ -349,7 +351,8 @@ export function addGroupToQueue({ if (!meta) { return {queue: queueItems, version: CURRENT_VERSION}; } - const actualIds = getActualItemsIds(config.items); + const configItems = config.items.concat(config.globalItems || []); + const actualIds = getActualItemsIds(configItems); const metaQueue = meta.queue || []; const notCurrent = (item: QueueItem) => { if (item.groupItemId) { diff --git a/src/shared/modules/state-and-params.ts b/src/shared/modules/state-and-params.ts index a4d4ac9..fc9d6fd 100644 --- a/src/shared/modules/state-and-params.ts +++ b/src/shared/modules/state-and-params.ts @@ -170,8 +170,12 @@ export function getItemsParams({ plugins, useStateAsInitial, }: GetItemsParamsArg): GetItemsParamsReturn { + const configItems = config.items.concat(config.globalItems || []); const {aliases, connections} = config; - const items = prerenderItems({items: config.items, plugins}); + const items = prerenderItems({ + items: configItems, + plugins, + }); const isFirstVersion = getCurrentVersion(itemsStateAndParams) === 1; const allItems = items.reduce((paramsItems: (ConfigItem | ConfigItemGroup)[], item) => { @@ -262,7 +266,8 @@ export function getItemsState({ config: Config; itemsStateAndParams: ItemsStateAndParams; }) { - return config.items.reduce((acc: Record, {id}) => { + const configItems = config.items.concat(config.globalItems || []); + return configItems.reduce((acc: Record, {id}) => { acc[id] = (itemsStateAndParams as ItemsStateAndParamsBase)?.[id]?.state || {}; return acc; }, {}); diff --git a/src/shared/modules/uniq-id.ts b/src/shared/modules/uniq-id.ts index 8e80149..c1c2741 100644 --- a/src/shared/modules/uniq-id.ts +++ b/src/shared/modules/uniq-id.ts @@ -7,7 +7,7 @@ import {isItemWithGroup, isItemWithTabs} from './helpers'; export function extractIdsFromConfig(config: Config): string[] { const ids: string[] = []; - const items = config.items || []; + const items = [...config.items, ...(config.globalItems || [])]; const connections = config.connections || []; const layout = config.layout || []; diff --git a/src/shared/types/config.ts b/src/shared/types/config.ts index 8e0ab30..62e4c8f 100644 --- a/src/shared/types/config.ts +++ b/src/shared/types/config.ts @@ -71,6 +71,7 @@ export interface Config { salt: string; counter: number; items: ConfigItem[]; + globalItems?: ConfigItem[]; layout: ConfigLayout[]; aliases: ConfigAliases; connections: ConfigConnection[]; diff --git a/src/typings/config.ts b/src/typings/config.ts index 6a0719f..4cb61cf 100644 --- a/src/typings/config.ts +++ b/src/typings/config.ts @@ -13,6 +13,7 @@ export type SetConfigItem = ConfigItem | AddConfigItem; export type SetItemOptions = { excludeIds?: string[]; + useGlobalItems?: boolean; }; export type GridReflowOptions = { diff --git a/src/utils/update-manager.ts b/src/utils/update-manager.ts index 7d9c2f7..600f52f 100644 --- a/src/utils/update-manager.ts +++ b/src/utils/update-manager.ts @@ -54,8 +54,9 @@ interface RemoveItemArg { } function removeItemVersion1({id, config, itemsStateAndParams}: RemoveItemArg) { - const itemIndex = config.items.findIndex((item) => item.id === id); - const removeItem = config.items[itemIndex]; + const configItems = config.items.concat(config.globalItems || []); + const itemIndex = configItems.findIndex((item) => item.id === id); + const removeItem = configItems[itemIndex]; const {defaults = {}} = removeItem; const itemParamsKeys = Object.keys(defaults); const getParams = (excludeId: string, items: ConfigItem[]) => { @@ -68,10 +69,10 @@ function removeItemVersion1({id, config, itemsStateAndParams}: RemoveItemArg) { }, {}), ); }; - const allParamsKeys = getParams(id, config.items); + const allParamsKeys = getParams(id, configItems); const allNamespaceParamsKeys = getParams( id, - config.items.filter((item) => item.namespace === removeItem.namespace), + configItems.filter((item) => item.namespace === removeItem.namespace), ); const uniqParamsKeys = itemParamsKeys.filter((key) => !allParamsKeys.includes(key)); const uniqNamespaceParamsKeys = itemParamsKeys.filter( @@ -83,7 +84,7 @@ function removeItemVersion1({id, config, itemsStateAndParams}: RemoveItemArg) { (acc, key) => { const {params} = (itemsStateAndParams as ItemsStateAndParamsBase)[key]; // в state из урла могут быть элементы, которых нет в config.items - const item = config.items.find((configItem) => configItem.id === key); + const item = configItems.find((configItem) => configItem.id === key); if (params && item) { const {namespace} = item; const currentUniqParamsKeys = @@ -193,10 +194,11 @@ function changeStateAndParamsVersion1({ stateAndParams, itemsStateAndParams, }: ChangeStateAndParamsArg) { + const allConfigItems = config.items.concat(config.globalItems || []); const hasState = 'state' in stateAndParams; const {aliases} = config; if ('params' in stateAndParams) { - const initiatorItem = config.items.find(({id}) => id === initiatorId) as ConfigItem; + const initiatorItem = allConfigItems.find(({id}) => id === initiatorId) as ConfigItem; const allowableParams = getAllowableChangedParams( initiatorItem, stateAndParams, @@ -211,7 +213,7 @@ function changeStateAndParamsVersion1({ .filter(({to}) => to === initiatorId) .map(({from}) => from); - const updateIds = config.items + const updateIds = allConfigItems .filter( (item) => item.namespace === initiatorItem.namespace && @@ -488,6 +490,13 @@ export class UpdateManager { const newItem = {...item, id: newIdData.id, data: newItemData.data, namespace}; const saveDefaultLayout = pick(layout, ['h', 'w', 'x', 'y', 'parent']); + const updateActions = { + counter: {$set: counter}, + ...(options.useGlobalItems + ? {globalItems: config.globalItems ? {$push: [newItem]} : {$set: [newItem]}} + : {items: {$push: [newItem]}}), + }; + if (options.updateLayout) { const byId = options.updateLayout.reduce>((memo, t) => { memo[t.i] = t; @@ -510,9 +519,8 @@ export class UpdateManager { } return update(config, { - items: {$push: [newItem]}, + ...updateActions, layout: {$set: newLayout}, - counter: {$set: counter}, }); } else if (isFinite(layout.y)) { let newLayout; @@ -529,18 +537,16 @@ export class UpdateManager { } return update(config, { - items: {$push: [newItem]}, + ...updateActions, layout: {$set: newLayout}, - counter: {$set: counter}, }); } else { const layoutY = bottom(config.layout); const newLayoutItem = {...saveDefaultLayout, y: layoutY, i: newItem.id}; return update(config, { - items: {$push: [newItem]}, + ...updateActions, layout: {$push: [newLayoutItem]}, - counter: {$set: counter}, }); } } @@ -556,8 +562,13 @@ export class UpdateManager { config: Config; options?: SetItemOptions; }) { + const globalItemIndex = config.globalItems?.findIndex(({id}) => item.id === id); const itemIndex = config.items.findIndex(({id}) => item.id === id); + const isGlobalItem = options.useGlobalItems; + const isCurrentlyInGlobalItems = globalItemIndex !== undefined && globalItemIndex !== -1; + const isCurrentlyInItems = itemIndex !== -1; + const {counter, data} = getNewItemData({ item, config, @@ -566,10 +577,34 @@ export class UpdateManager { options, }); - return update(config, { - items: {[itemIndex]: {$set: {...item, data, namespace}}}, - counter: {$set: counter}, - }); + const updatedItem = {...item, data, namespace}; + + // Determine if we need to move the item between arrays + if (isGlobalItem && isCurrentlyInItems) { + // Move from items to globalItems + return update(config, { + items: {$splice: [[itemIndex, 1]]}, + globalItems: config.globalItems ? {$push: [updatedItem]} : {$set: [updatedItem]}, + counter: {$set: counter}, + }); + } else if (!isGlobalItem && isCurrentlyInGlobalItems) { + // Move from globalItems to items + return update(config, { + globalItems: {$splice: [[globalItemIndex, 1]]}, + items: {$push: [updatedItem]}, + counter: {$set: counter}, + }); + } else { + // Update in current location + const updateAction = isCurrentlyInGlobalItems + ? {globalItems: {[globalItemIndex]: {$set: updatedItem}}} + : {items: {[itemIndex]: {$set: updatedItem}}}; + + return update(config, { + ...updateAction, + counter: {$set: counter}, + }); + } } static removeItem({id, config, itemsStateAndParams = {}}: RemoveItemArg): { @@ -579,9 +614,11 @@ export class UpdateManager { if (getCurrentVersion(itemsStateAndParams) === 1) { return removeItemVersion1({id, config, itemsStateAndParams}); } + const globalItemIndex = config.globalItems?.findIndex((item) => item.id === id) ?? -1; const itemIndex = config.items.findIndex((item) => item.id === id); const layoutIndex = config.layout.findIndex((item) => item.i === id); - const item = config.items[itemIndex]; + const item = config.items[itemIndex] || config.globalItems?.[globalItemIndex]; + let itemIds = [id]; if (isItemWithTabs(item)) { itemIds = [id].concat(item.data.tabs.map((tab) => tab.id)); @@ -592,17 +629,25 @@ export class UpdateManager { const connections = config.connections.filter( ({from, to}) => !itemIds.includes(from) && !itemIds.includes(to), ); + + const updateAction: {[key: string]: Spec} = + globalItemIndex !== undefined && globalItemIndex !== -1 + ? {globalItems: {$splice: [[globalItemIndex, 1]]}} + : { + items: { + $splice: [[itemIndex, 1]], + }, + }; + return { config: update(config, { - items: { - $splice: [[itemIndex, 1]], - }, layout: { $splice: [[layoutIndex, 1]], }, connections: { $set: connections, }, + ...updateAction, }), itemsStateAndParams: update(itemsStateAndParams, { $unset: [id], @@ -632,6 +677,8 @@ export class UpdateManager { itemsStateAndParams, options, }: ChangeStateAndParamsArg): ItemsStateAndParams { + const allConfigItems = config.items.concat(config.globalItems || []); + if (getCurrentVersion(itemsStateAndParams) === 1) { return changeStateAndParamsVersion1({ id: initiatorId, @@ -643,7 +690,7 @@ export class UpdateManager { const action = options?.action; const hasState = 'state' in stateAndParams; - const {items} = config; + const items = allConfigItems; const itemsIds = items.map(({id: itemId}) => itemId); const itemsStateAndParamsIds = Object.keys(omit(itemsStateAndParams, [META_KEY])); const unusedIds = itemsStateAndParamsIds.filter((id) => !itemsIds.includes(id)); @@ -690,7 +737,12 @@ export class UpdateManager { const tabId: string | undefined = isItemWithTabs(initiatorItem) ? newTabId || resolveItemInnerId({item: initiatorItem, itemsStateAndParams}) : undefined; - const meta = addToQueue({id: initiatorId, tabId, config, itemsStateAndParams}); + const meta = addToQueue({ + id: initiatorId, + tabId, + config, + itemsStateAndParams, + }); let commandUpdateParams: string = (itemsStateAndParams as ItemsStateAndParamsBase)[ initiatorId ]?.params From a91d43ab313a4e742aacaab5f6814699ff1e95cb Mon Sep 17 00:00:00 2001 From: Taya Leutina Date: Mon, 10 Nov 2025 12:36:32 +0300 Subject: [PATCH 2/5] fix: add additional check --- src/hooks/useCalcLayout.ts | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/hooks/useCalcLayout.ts b/src/hooks/useCalcLayout.ts index 7358a69..93f7c6b 100644 --- a/src/hooks/useCalcLayout.ts +++ b/src/hooks/useCalcLayout.ts @@ -2,18 +2,28 @@ import React from 'react'; import isEqual from 'lodash/isEqual'; -import type {Config} from '../shared'; +import type {Config, ConfigItem, ConfigLayout} from '../shared'; import {RegisterManager} from '../utils'; function onUpdatePropsConfig(config: Config, registerManager: RegisterManager) { const configItems = [...config.items, ...(config.globalItems || [])]; - return config.layout.map((itemLayout, i) => { - const {type} = configItems[i]; - return { - ...registerManager.getItem(type).defaultLayout, - ...itemLayout, - }; - }); + + return config.layout.reduce((acc, itemLayout, i) => { + const item: ConfigItem | undefined = configItems[i]; + const foundItem = + item && item.id === itemLayout.i + ? item + : configItems.find((configItem) => configItem.id === itemLayout.i); + + if (foundItem) { + acc.push({ + ...registerManager.getItem(foundItem.type).defaultLayout, + ...itemLayout, + }); + } + + return acc; + }, []); } export const useCalcPropsLayout = (config: Config, registerManager: RegisterManager) => { From ea6958bb513d28b8437a65d429494ba64c05d8c5 Mon Sep 17 00:00:00 2001 From: Taya Leutina Date: Mon, 10 Nov 2025 16:22:34 +0300 Subject: [PATCH 3/5] fix: optimization --- src/components/GridLayout/GridLayout.js | 6 +- src/components/MobileLayout/MobileLayout.tsx | 5 +- src/hocs/withContext.js | 1 - .../modules/__tests__/global-items.test.ts | 164 ++++++++++-------- src/shared/modules/helpers.ts | 10 +- src/shared/modules/state-and-params.ts | 5 +- src/shared/modules/uniq-id.ts | 4 +- src/utils/update-manager.ts | 33 ++-- 8 files changed, 126 insertions(+), 102 deletions(-) diff --git a/src/components/GridLayout/GridLayout.js b/src/components/GridLayout/GridLayout.js index e6d43b2..f36c85d 100644 --- a/src/components/GridLayout/GridLayout.js +++ b/src/components/GridLayout/GridLayout.js @@ -697,9 +697,7 @@ export default class GridLayout extends React.PureComponent { render() { const {config, groups, editMode, context} = this.context; - const configItems = this.context.configItems; - - this.pluginsRefs.length = configItems.length; + this.pluginsRefs.length = this.context.configItems.length; const defaultRenderLayout = []; const defaultRenderItems = []; @@ -720,7 +718,7 @@ export default class GridLayout extends React.PureComponent { return memo; }, []); - const itemsByGroup = configItems.reduce((memo, item) => { + const itemsByGroup = this.context.configItems.reduce((memo, item) => { const group = layoutMap[item.id]; if (group) { if (!memo[group]) { diff --git a/src/components/MobileLayout/MobileLayout.tsx b/src/components/MobileLayout/MobileLayout.tsx index 613b7cf..6714617 100644 --- a/src/components/MobileLayout/MobileLayout.tsx +++ b/src/components/MobileLayout/MobileLayout.tsx @@ -110,12 +110,11 @@ export default class MobileLayout extends React.PureComponent< } this._memoLayout = this.context.layout; - const configItems = this.context.configItems; - const hasOrderId = Boolean(configItems.find((item) => item.orderId)); + const hasOrderId = Boolean(this.context.configItems.find((item) => item.orderId)); this.sortedLayoutItems = groupBy( - getSortedConfigItems(this.context.config, configItems, hasOrderId), + getSortedConfigItems(this.context.config, this.context.configItems, hasOrderId), (item) => item.parent || DEFAULT_GROUP, ); diff --git a/src/hocs/withContext.js b/src/hocs/withContext.js index 8793dcb..c1eafb2 100644 --- a/src/hocs/withContext.js +++ b/src/hocs/withContext.js @@ -140,7 +140,6 @@ function useMemoStateContext(props) { onChange, props.config, props.itemsStateAndParams, - configItems, setTemporaryLayout, ], ); diff --git a/src/shared/modules/__tests__/global-items.test.ts b/src/shared/modules/__tests__/global-items.test.ts index fcc6b56..a15f9d4 100644 --- a/src/shared/modules/__tests__/global-items.test.ts +++ b/src/shared/modules/__tests__/global-items.test.ts @@ -10,45 +10,33 @@ import {addGroupToQueue, addToQueue, formQueueData} from '../helpers'; import {getItemsParams, getItemsStateAndParams} from '../state-and-params'; const NAMESPACE = 'default'; +const GROUP_CONTROL_TYPE = 'group-control'; + const GLOBAL_ITEM_ID = 'globalItem1'; const REGULAR_ITEM_ID = 'regularItem1'; const GROUP_ITEM_ID = 'groupItem1'; - -const getMockedGlobalItem = (id: string = GLOBAL_ITEM_ID, defaults?: StringParams): ConfigItem => ({ - id, - defaults, - data: {}, - type: 'control', - namespace: NAMESPACE, -}); - -const getMockedRegularItem = ( - id: string = REGULAR_ITEM_ID, - defaults?: StringParams, -): ConfigItem => ({ - id, - defaults, - data: {}, - type: 'control', - namespace: NAMESPACE, -}); - -const getMockedGroupItem = ( - id: string, - groupItemIds: string[] = [GROUP_ITEM_ID], - defaults?: StringParams, -): ConfigItem => ({ - id, - data: { - group: groupItemIds.map((groupItemId) => ({ - id: groupItemId, - namespace: NAMESPACE, - defaults, - })), - }, - type: 'group-control', - namespace: NAMESPACE, -}); +const GROUP_ITEM_ID_2 = 'groupItem2'; + +const getMockedControlItem = (props?: { + id: string; + groupItemIds?: string[]; + defaults?: StringParams; +}): ConfigItem => { + const {id = REGULAR_ITEM_ID, groupItemIds = [GROUP_ITEM_ID], defaults} = props || {}; + return { + id, + defaults, + data: { + group: groupItemIds.map((groupItemId) => ({ + id: groupItemId, + namespace: NAMESPACE, + defaults, + })), + }, + type: GROUP_CONTROL_TYPE, + namespace: NAMESPACE, + }; +}; const getMockedConfig = ({ items = [], @@ -69,8 +57,8 @@ const getMockedConfig = ({ describe('globalItems functionality in config', () => { describe('addToQueue with globalItems', () => { it('should include globalItems when filtering actual IDs', () => { - const globalItem = getMockedGlobalItem(); - const regularItem = getMockedRegularItem(); + const globalItem = getMockedControlItem({id: GLOBAL_ITEM_ID}); + const regularItem = getMockedControlItem(); const config = getMockedConfig({ items: [regularItem], globalItems: [globalItem], @@ -94,7 +82,7 @@ describe('globalItems functionality in config', () => { }); it('should handle empty globalItems array', () => { - const regularItem = getMockedRegularItem(); + const regularItem = getMockedControlItem(); const config = getMockedConfig({ items: [regularItem], globalItems: [], @@ -122,11 +110,11 @@ describe('globalItems functionality in config', () => { const globalGroupItemId = 'globalGroupItem'; const globalGroupSubItemId = 'globalGroupSubItem'; const globalGroupSubItemId2 = 'globalGroupSubItem2'; - const globalGroupItem = getMockedGroupItem(globalGroupItemId, [ - globalGroupSubItemId, - globalGroupSubItemId2, - ]); - const regularItem = getMockedRegularItem(); + const globalGroupItem = getMockedControlItem({ + id: globalGroupItemId, + groupItemIds: [globalGroupSubItemId, globalGroupSubItemId2], + }); + const regularItem = getMockedControlItem(); const config = getMockedConfig({ items: [regularItem], globalItems: [globalGroupItem], @@ -159,10 +147,18 @@ describe('globalItems functionality in config', () => { describe('formQueueData with globalItems', () => { it('should process globalItems in queue data formation', () => { - const globalItem = getMockedGroupItem(GLOBAL_ITEM_ID, [GROUP_ITEM_ID], { - size: 'l', + const globalItem = getMockedControlItem({ + id: GLOBAL_ITEM_ID, + groupItemIds: [GROUP_ITEM_ID], + defaults: { + size: 'l', + }, + }); + const regularItem = getMockedControlItem({ + id: REGULAR_ITEM_ID, + groupItemIds: [GROUP_ITEM_ID_2], + defaults: {view: 'normal'}, }); - const regularItem = getMockedRegularItem(REGULAR_ITEM_ID, {view: 'normal'}); const itemsStateAndParams: ItemsStateAndParams = { [GLOBAL_ITEM_ID]: { @@ -175,13 +171,15 @@ describe('globalItems functionality in config', () => { }, [REGULAR_ITEM_ID]: { params: { - view: 'contrast', + [GROUP_ITEM_ID_2]: { + view: 'contrast', + }, }, }, [META_KEY]: { queue: [ {id: GLOBAL_ITEM_ID, groupItemId: GROUP_ITEM_ID}, - {id: REGULAR_ITEM_ID}, + {id: REGULAR_ITEM_ID, groupItemId: GROUP_ITEM_ID_2}, ], version: 2, }, @@ -199,7 +197,7 @@ describe('globalItems functionality in config', () => { params: {size: 'xl'}, }, { - id: REGULAR_ITEM_ID, + id: GROUP_ITEM_ID_2, namespace: NAMESPACE, params: {view: 'contrast'}, }, @@ -212,9 +210,13 @@ describe('globalItems functionality in config', () => { const globalParam = 'globalParam'; const overriddenParam = 'overriddenValue'; const regularParam = 'regularValue'; - const globalItem = getMockedGlobalItem(GLOBAL_ITEM_ID, {globalParam}); - const regularItem = getMockedRegularItem(REGULAR_ITEM_ID, { - regularParam, + const globalItem = getMockedControlItem({id: GLOBAL_ITEM_ID, defaults: {globalParam}}); + const regularItem = getMockedControlItem({ + id: REGULAR_ITEM_ID, + groupItemIds: [GROUP_ITEM_ID_2], + defaults: { + regularParam, + }, }); const config = getMockedConfig({ @@ -224,10 +226,14 @@ describe('globalItems functionality in config', () => { const itemsStateAndParams: ItemsStateAndParams = { [GLOBAL_ITEM_ID]: { - params: {globalParam: overriddenParam}, + params: { + [GROUP_ITEM_ID]: { + globalParam: overriddenParam, + }, + }, }, [META_KEY]: { - queue: [{id: GLOBAL_ITEM_ID}], + queue: [{id: GLOBAL_ITEM_ID, groupItemId: GROUP_ITEM_ID}], version: 2, }, }; @@ -239,18 +245,22 @@ describe('globalItems functionality in config', () => { itemsStateAndParams, plugins: [ { - type: 'control', + type: GROUP_CONTROL_TYPE, }, ], }); expect(result[GLOBAL_ITEM_ID]).toEqual({ - globalParam: overriddenParam, - regularParam, + [GROUP_ITEM_ID]: { + globalParam: overriddenParam, + regularParam, + }, }); expect(result[REGULAR_ITEM_ID]).toEqual({ - globalParam: overriddenParam, - regularParam, + [GROUP_ITEM_ID_2]: { + globalParam: overriddenParam, + regularParam, + }, }); }); }); @@ -262,9 +272,16 @@ describe('globalItems functionality in config', () => { const regularParam = 'regularValue'; const globalState = 'globalStateValue'; const regularState = 'regularStateValue'; - const globalItem = getMockedGlobalItem(GLOBAL_ITEM_ID, {globalDefault: initialValue}); - const regularItem = getMockedRegularItem(REGULAR_ITEM_ID, { - regularDefault: initialValue, + const globalItem = getMockedControlItem({ + id: GLOBAL_ITEM_ID, + defaults: {globalDefault: initialValue}, + }); + const regularItem = getMockedControlItem({ + id: REGULAR_ITEM_ID, + groupItemIds: [GROUP_ITEM_ID_2], + defaults: { + regularDefault: initialValue, + }, }); const config = getMockedConfig({ @@ -295,26 +312,27 @@ describe('globalItems functionality in config', () => { itemsStateAndParams, plugins: [ { - type: 'control', - }, - { - type: 'group-control', + type: GROUP_CONTROL_TYPE, }, ], }) as ItemsStateAndParamsBase; expect(result[GLOBAL_ITEM_ID]).toEqual({ params: { - globalDefault: initialValue, - regularDefault: initialValue, + [GROUP_ITEM_ID]: { + globalDefault: initialValue, + regularDefault: initialValue, + }, }, state: {globalState}, }); expect(result[REGULAR_ITEM_ID]).toEqual({ params: { - globalDefault: initialValue, - regularDefault: initialValue, + [GROUP_ITEM_ID_2]: { + globalDefault: initialValue, + regularDefault: initialValue, + }, }, state: {regularState}, }); @@ -330,8 +348,8 @@ describe('globalItems functionality in config', () => { it('should handle config with only globalItems and no regular items', () => { const globalItemId1 = 'global1'; const globalItemId2 = 'global2'; - const globalItem1 = getMockedGlobalItem(globalItemId1); - const globalItem2 = getMockedGlobalItem(globalItemId2); + const globalItem1 = getMockedControlItem({id: globalItemId1}); + const globalItem2 = getMockedControlItem({id: globalItemId2}); const config = getMockedConfig({ items: [], diff --git a/src/shared/modules/helpers.ts b/src/shared/modules/helpers.ts index bcc65c4..5400c8b 100644 --- a/src/shared/modules/helpers.ts +++ b/src/shared/modules/helpers.ts @@ -320,7 +320,7 @@ export function addToQueue({ return {queue: [queueItem], version: CURRENT_VERSION}; } - const configItems = config.items.concat(config.globalItems || []); + const configItems = getAllConfigItems(config); const actualIds = getActualItemsIds(configItems); const metaQueue = meta.queue || []; const notCurrent = (item: QueueItem) => { @@ -351,7 +351,7 @@ export function addGroupToQueue({ if (!meta) { return {queue: queueItems, version: CURRENT_VERSION}; } - const configItems = config.items.concat(config.globalItems || []); + const configItems = getAllConfigItems(config); const actualIds = getActualItemsIds(configItems); const metaQueue = meta.queue || []; const notCurrent = (item: QueueItem) => { @@ -461,3 +461,9 @@ export function hasActionParams(stateAndParams: ItemStateAndParams) { return hasActionParam(stateAndParams.params); } + +// Combines regular items and global items from config into a single array +// For global usage across the project +export function getAllConfigItems(config: Config): ConfigItem[] { + return [...config.items, ...(config.globalItems || [])]; +} diff --git a/src/shared/modules/state-and-params.ts b/src/shared/modules/state-and-params.ts index fc9d6fd..17b5bc1 100644 --- a/src/shared/modules/state-and-params.ts +++ b/src/shared/modules/state-and-params.ts @@ -19,6 +19,7 @@ import { import { FormedQueueData, formQueueData, + getAllConfigItems, getCurrentVersion, getMapItemsIgnores, hasActionParam, @@ -170,7 +171,7 @@ export function getItemsParams({ plugins, useStateAsInitial, }: GetItemsParamsArg): GetItemsParamsReturn { - const configItems = config.items.concat(config.globalItems || []); + const configItems = getAllConfigItems(config); const {aliases, connections} = config; const items = prerenderItems({ items: configItems, @@ -266,7 +267,7 @@ export function getItemsState({ config: Config; itemsStateAndParams: ItemsStateAndParams; }) { - const configItems = config.items.concat(config.globalItems || []); + const configItems = getAllConfigItems(config); return configItems.reduce((acc: Record, {id}) => { acc[id] = (itemsStateAndParams as ItemsStateAndParamsBase)?.[id]?.state || {}; return acc; diff --git a/src/shared/modules/uniq-id.ts b/src/shared/modules/uniq-id.ts index c1c2741..744bb76 100644 --- a/src/shared/modules/uniq-id.ts +++ b/src/shared/modules/uniq-id.ts @@ -2,12 +2,12 @@ import Hashids from 'hashids'; import type {Config} from '../types'; -import {isItemWithGroup, isItemWithTabs} from './helpers'; +import {getAllConfigItems, isItemWithGroup, isItemWithTabs} from './helpers'; export function extractIdsFromConfig(config: Config): string[] { const ids: string[] = []; - const items = [...config.items, ...(config.globalItems || [])]; + const items = getAllConfigItems(config); const connections = config.connections || []; const layout = config.layout || []; diff --git a/src/utils/update-manager.ts b/src/utils/update-manager.ts index 600f52f..d7bb657 100644 --- a/src/utils/update-manager.ts +++ b/src/utils/update-manager.ts @@ -9,6 +9,7 @@ import { addGroupToQueue, addToQueue, deleteFromQueue, + getAllConfigItems, getCurrentVersion, getInitialItemsStateAndParamsMeta, getItemsStateAndParamsMeta, @@ -54,7 +55,7 @@ interface RemoveItemArg { } function removeItemVersion1({id, config, itemsStateAndParams}: RemoveItemArg) { - const configItems = config.items.concat(config.globalItems || []); + const configItems = getAllConfigItems(config); const itemIndex = configItems.findIndex((item) => item.id === id); const removeItem = configItems[itemIndex]; const {defaults = {}} = removeItem; @@ -194,7 +195,7 @@ function changeStateAndParamsVersion1({ stateAndParams, itemsStateAndParams, }: ChangeStateAndParamsArg) { - const allConfigItems = config.items.concat(config.globalItems || []); + const allConfigItems = getAllConfigItems(config); const hasState = 'state' in stateAndParams; const {aliases} = config; if ('params' in stateAndParams) { @@ -562,11 +563,13 @@ export class UpdateManager { config: Config; options?: SetItemOptions; }) { - const globalItemIndex = config.globalItems?.findIndex(({id}) => item.id === id); + const globalItemIndex = config.globalItems + ? config.globalItems.findIndex(({id}) => item.id === id) + : -1; const itemIndex = config.items.findIndex(({id}) => item.id === id); - const isGlobalItem = options.useGlobalItems; - const isCurrentlyInGlobalItems = globalItemIndex !== undefined && globalItemIndex !== -1; + const shouldBeGlobalItem = options.useGlobalItems; + const isCurrentlyInGlobalItems = globalItemIndex !== -1; const isCurrentlyInItems = itemIndex !== -1; const {counter, data} = getNewItemData({ @@ -580,14 +583,14 @@ export class UpdateManager { const updatedItem = {...item, data, namespace}; // Determine if we need to move the item between arrays - if (isGlobalItem && isCurrentlyInItems) { + if (shouldBeGlobalItem && isCurrentlyInItems) { // Move from items to globalItems return update(config, { items: {$splice: [[itemIndex, 1]]}, globalItems: config.globalItems ? {$push: [updatedItem]} : {$set: [updatedItem]}, counter: {$set: counter}, }); - } else if (!isGlobalItem && isCurrentlyInGlobalItems) { + } else if (!shouldBeGlobalItem && isCurrentlyInGlobalItems) { // Move from globalItems to items return update(config, { globalItems: {$splice: [[globalItemIndex, 1]]}, @@ -614,7 +617,9 @@ export class UpdateManager { if (getCurrentVersion(itemsStateAndParams) === 1) { return removeItemVersion1({id, config, itemsStateAndParams}); } - const globalItemIndex = config.globalItems?.findIndex((item) => item.id === id) ?? -1; + const globalItemIndex = config.globalItems + ? config.globalItems.findIndex((item) => item.id === id) + : -1; const itemIndex = config.items.findIndex((item) => item.id === id); const layoutIndex = config.layout.findIndex((item) => item.i === id); const item = config.items[itemIndex] || config.globalItems?.[globalItemIndex]; @@ -631,13 +636,13 @@ export class UpdateManager { ); const updateAction: {[key: string]: Spec} = - globalItemIndex !== undefined && globalItemIndex !== -1 - ? {globalItems: {$splice: [[globalItemIndex, 1]]}} - : { + globalItemIndex === -1 + ? { items: { $splice: [[itemIndex, 1]], }, - }; + } + : {globalItems: {$splice: [[globalItemIndex, 1]]}}; return { config: update(config, { @@ -677,8 +682,6 @@ export class UpdateManager { itemsStateAndParams, options, }: ChangeStateAndParamsArg): ItemsStateAndParams { - const allConfigItems = config.items.concat(config.globalItems || []); - if (getCurrentVersion(itemsStateAndParams) === 1) { return changeStateAndParamsVersion1({ id: initiatorId, @@ -690,7 +693,7 @@ export class UpdateManager { const action = options?.action; const hasState = 'state' in stateAndParams; - const items = allConfigItems; + const items = getAllConfigItems(config); const itemsIds = items.map(({id: itemId}) => itemId); const itemsStateAndParamsIds = Object.keys(omit(itemsStateAndParams, [META_KEY])); const unusedIds = itemsStateAndParamsIds.filter((id) => !itemsIds.includes(id)); From b804a2042d2b5fab18705642c560496dc27174a3 Mon Sep 17 00:00:00 2001 From: Taya Leutina Date: Mon, 10 Nov 2025 18:05:48 +0300 Subject: [PATCH 4/5] fix: use getAllConfigItems in all places --- src/hocs/withContext.js | 5 +++-- src/hooks/useCalcLayout.ts | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/hocs/withContext.js b/src/hocs/withContext.js index c1eafb2..6f0e6e7 100644 --- a/src/hocs/withContext.js +++ b/src/hocs/withContext.js @@ -12,7 +12,7 @@ import { } from '../constants/common'; import {DashKitContext, DashKitDnDContext, DashkitOvelayControlsContext} from '../context'; import {useDeepEqualMemo} from '../hooks/useDeepEqualMemo'; -import {getItemsParams, getItemsState} from '../shared'; +import {getAllConfigItems, getItemsParams, getItemsState} from '../shared'; import {UpdateManager, resolveLayoutGroup} from '../utils'; const ITEM_PROPS = ['i', 'h', 'w', 'x', 'y', 'parent']; @@ -104,8 +104,9 @@ function useMemoStateContext(props) { [props.config.layout], ); + // to calculate items, only memorization of items and globalItems is important const configItems = React.useMemo( - () => props.config.items.concat(props.config.globalItems || []), + () => getAllConfigItems(props.config), [props.config.items, props.config.globalItems], ); diff --git a/src/hooks/useCalcLayout.ts b/src/hooks/useCalcLayout.ts index 93f7c6b..906ecc7 100644 --- a/src/hooks/useCalcLayout.ts +++ b/src/hooks/useCalcLayout.ts @@ -2,11 +2,11 @@ import React from 'react'; import isEqual from 'lodash/isEqual'; -import type {Config, ConfigItem, ConfigLayout} from '../shared'; +import {type Config, type ConfigItem, type ConfigLayout, getAllConfigItems} from '../shared'; import {RegisterManager} from '../utils'; function onUpdatePropsConfig(config: Config, registerManager: RegisterManager) { - const configItems = [...config.items, ...(config.globalItems || [])]; + const configItems = getAllConfigItems(config); return config.layout.reduce((acc, itemLayout, i) => { const item: ConfigItem | undefined = configItems[i]; From 152616f3d3ff62802371accf0a1e520162eb2278 Mon Sep 17 00:00:00 2001 From: Taya Leutina Date: Mon, 10 Nov 2025 21:10:22 +0300 Subject: [PATCH 5/5] feat: add test for config without globalItems --- .../modules/__tests__/global-items.test.ts | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/shared/modules/__tests__/global-items.test.ts b/src/shared/modules/__tests__/global-items.test.ts index a15f9d4..a9fcda3 100644 --- a/src/shared/modules/__tests__/global-items.test.ts +++ b/src/shared/modules/__tests__/global-items.test.ts @@ -342,6 +342,61 @@ describe('globalItems functionality in config', () => { version: 2, }); }); + + it('should correctly handle config without globalItems', () => { + const initialValue = 'value'; + const regularParam = 'regularValue'; + const regularState = 'regularStateValue'; + + const regularItem = getMockedControlItem({ + id: REGULAR_ITEM_ID, + defaults: { + regularParam: initialValue, + }, + }); + + const config = getMockedConfig({ + items: [regularItem], + globalItems: undefined, + }); + + const itemsStateAndParams: ItemsStateAndParams = { + [REGULAR_ITEM_ID]: { + params: {regularParam: initialValue}, + state: {regularState}, + }, + [META_KEY]: { + queue: [{id: REGULAR_ITEM_ID, groupItemId: GROUP_ITEM_ID}], + version: 2, + }, + }; + + const result = getItemsStateAndParams({ + defaultGlobalParams: {}, + globalParams: {regularParam}, + config, + itemsStateAndParams, + plugins: [ + { + type: GROUP_CONTROL_TYPE, + }, + ], + }) as ItemsStateAndParamsBase; + + expect(result[REGULAR_ITEM_ID]).toEqual({ + params: { + [GROUP_ITEM_ID]: { + regularParam, + }, + }, + state: {regularState}, + }); + + expect(result[META_KEY]).toEqual({ + queue: [{id: REGULAR_ITEM_ID, groupItemId: GROUP_ITEM_ID}], + version: 2, + }); + }); }); describe('edge cases with globalItems', () => {