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..f36c85d 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,7 @@ export default class GridLayout extends React.PureComponent { render() { const {config, groups, editMode, context} = this.context; - this.pluginsRefs.length = config.items.length; + this.pluginsRefs.length = this.context.configItems.length; const defaultRenderLayout = []; const defaultRenderItems = []; @@ -718,7 +718,7 @@ export default class GridLayout extends React.PureComponent { return memo; }, []); - const itemsByGroup = config.items.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 12b75a5..6714617 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; @@ -104,10 +111,10 @@ export default class MobileLayout extends React.PureComponent< this._memoLayout = this.context.layout; - const hasOrderId = Boolean(this.context.config.items.find((item) => item.orderId)); + const hasOrderId = Boolean(this.context.configItems.find((item) => item.orderId)); this.sortedLayoutItems = groupBy( - getSortedConfigItems(this.context.config, hasOrderId), + getSortedConfigItems(this.context.config, this.context.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..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,6 +104,12 @@ function useMemoStateContext(props) { [props.config.layout], ); + // to calculate items, only memorization of items and globalItems is important + const configItems = React.useMemo( + () => getAllConfigItems(props.config), + [props.config.items, props.config.globalItems], + ); + const onItemRemove = React.useCallback( (id) => { delete nowrapAdjustedLayouts.current[id]; @@ -130,12 +136,12 @@ function useMemoStateContext(props) { } }, [ - props.config, - props.itemsStateAndParams, + resetTemporaryLayout, temporaryLayout, onChange, + props.config, + props.itemsStateAndParams, 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..906ecc7 100644 --- a/src/hooks/useCalcLayout.ts +++ b/src/hooks/useCalcLayout.ts @@ -2,21 +2,33 @@ import React from 'react'; import isEqual from 'lodash/isEqual'; -import type {Config} from '../shared'; +import {type Config, type ConfigItem, type ConfigLayout, getAllConfigItems} from '../shared'; import {RegisterManager} from '../utils'; function onUpdatePropsConfig(config: Config, registerManager: RegisterManager) { - return config.layout.map((itemLayout, i) => { - const {type} = config.items[i]; - return { - ...registerManager.getItem(type).defaultLayout, - ...itemLayout, - }; - }); + const configItems = getAllConfigItems(config); + + 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) => { 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..a9fcda3 --- /dev/null +++ b/src/shared/modules/__tests__/global-items.test.ts @@ -0,0 +1,430 @@ +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 GROUP_CONTROL_TYPE = 'group-control'; + +const GLOBAL_ITEM_ID = 'globalItem1'; +const REGULAR_ITEM_ID = 'regularItem1'; +const GROUP_ITEM_ID = 'groupItem1'; +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 = [], + 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 = getMockedControlItem({id: GLOBAL_ITEM_ID}); + const regularItem = getMockedControlItem(); + 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 = getMockedControlItem(); + 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 = getMockedControlItem({ + id: globalGroupItemId, + groupItemIds: [globalGroupSubItemId, globalGroupSubItemId2], + }); + const regularItem = getMockedControlItem(); + 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 = 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 itemsStateAndParams: ItemsStateAndParams = { + [GLOBAL_ITEM_ID]: { + params: { + [GROUP_ITEM_ID]: { + size: 'xl', + color: 'red', + }, + }, + }, + [REGULAR_ITEM_ID]: { + params: { + [GROUP_ITEM_ID_2]: { + view: 'contrast', + }, + }, + }, + [META_KEY]: { + queue: [ + {id: GLOBAL_ITEM_ID, groupItemId: GROUP_ITEM_ID}, + {id: REGULAR_ITEM_ID, groupItemId: GROUP_ITEM_ID_2}, + ], + version: 2, + }, + }; + + const result = formQueueData({ + items: [regularItem, globalItem], + itemsStateAndParams, + }); + + expect(result).toEqual([ + { + id: GROUP_ITEM_ID, + namespace: NAMESPACE, + params: {size: 'xl'}, + }, + { + id: GROUP_ITEM_ID_2, + 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 = getMockedControlItem({id: GLOBAL_ITEM_ID, defaults: {globalParam}}); + const regularItem = getMockedControlItem({ + id: REGULAR_ITEM_ID, + groupItemIds: [GROUP_ITEM_ID_2], + defaults: { + regularParam, + }, + }); + + const config = getMockedConfig({ + items: [regularItem], + globalItems: [globalItem], + }); + + const itemsStateAndParams: ItemsStateAndParams = { + [GLOBAL_ITEM_ID]: { + params: { + [GROUP_ITEM_ID]: { + globalParam: overriddenParam, + }, + }, + }, + [META_KEY]: { + queue: [{id: GLOBAL_ITEM_ID, groupItemId: GROUP_ITEM_ID}], + version: 2, + }, + }; + + const result = getItemsParams({ + defaultGlobalParams: {}, + globalParams: {}, + config, + itemsStateAndParams, + plugins: [ + { + type: GROUP_CONTROL_TYPE, + }, + ], + }); + + expect(result[GLOBAL_ITEM_ID]).toEqual({ + [GROUP_ITEM_ID]: { + globalParam: overriddenParam, + regularParam, + }, + }); + expect(result[REGULAR_ITEM_ID]).toEqual({ + [GROUP_ITEM_ID_2]: { + 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 = 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({ + 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: GROUP_CONTROL_TYPE, + }, + ], + }) as ItemsStateAndParamsBase; + + expect(result[GLOBAL_ITEM_ID]).toEqual({ + params: { + [GROUP_ITEM_ID]: { + globalDefault: initialValue, + regularDefault: initialValue, + }, + }, + state: {globalState}, + }); + + expect(result[REGULAR_ITEM_ID]).toEqual({ + params: { + [GROUP_ITEM_ID_2]: { + globalDefault: initialValue, + regularDefault: initialValue, + }, + }, + state: {regularState}, + }); + + expect(result[META_KEY]).toEqual({ + queue: [{id: GLOBAL_ITEM_ID}, {id: REGULAR_ITEM_ID}], + 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', () => { + it('should handle config with only globalItems and no regular items', () => { + const globalItemId1 = 'global1'; + const globalItemId2 = 'global2'; + const globalItem1 = getMockedControlItem({id: globalItemId1}); + const globalItem2 = getMockedControlItem({id: 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..5400c8b 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 = getAllConfigItems(config); + 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 = getAllConfigItems(config); + const actualIds = getActualItemsIds(configItems); const metaQueue = meta.queue || []; const notCurrent = (item: QueueItem) => { if (item.groupItemId) { @@ -458,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 a4d4ac9..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,8 +171,12 @@ export function getItemsParams({ plugins, useStateAsInitial, }: GetItemsParamsArg): GetItemsParamsReturn { + const configItems = getAllConfigItems(config); 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 +267,8 @@ export function getItemsState({ config: Config; itemsStateAndParams: ItemsStateAndParams; }) { - return config.items.reduce((acc: Record, {id}) => { + 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 8e80149..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 || []; + const items = getAllConfigItems(config); 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..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,8 +55,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 = getAllConfigItems(config); + 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 +70,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 +85,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 +195,11 @@ function changeStateAndParamsVersion1({ stateAndParams, itemsStateAndParams, }: ChangeStateAndParamsArg) { + const allConfigItems = getAllConfigItems(config); 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 +214,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 +491,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 +520,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 +538,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 +563,15 @@ export class UpdateManager { config: Config; options?: SetItemOptions; }) { + const globalItemIndex = config.globalItems + ? config.globalItems.findIndex(({id}) => item.id === id) + : -1; const itemIndex = config.items.findIndex(({id}) => item.id === id); + const shouldBeGlobalItem = options.useGlobalItems; + const isCurrentlyInGlobalItems = globalItemIndex !== -1; + const isCurrentlyInItems = itemIndex !== -1; + const {counter, data} = getNewItemData({ item, config, @@ -566,10 +580,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 (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 (!shouldBeGlobalItem && 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 +617,13 @@ export class UpdateManager { if (getCurrentVersion(itemsStateAndParams) === 1) { return removeItemVersion1({id, config, itemsStateAndParams}); } + 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]; + 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 +634,25 @@ export class UpdateManager { const connections = config.connections.filter( ({from, to}) => !itemIds.includes(from) && !itemIds.includes(to), ); + + const updateAction: {[key: string]: Spec} = + globalItemIndex === -1 + ? { + items: { + $splice: [[itemIndex, 1]], + }, + } + : {globalItems: {$splice: [[globalItemIndex, 1]]}}; + return { config: update(config, { - items: { - $splice: [[itemIndex, 1]], - }, layout: { $splice: [[layoutIndex, 1]], }, connections: { $set: connections, }, + ...updateAction, }), itemsStateAndParams: update(itemsStateAndParams, { $unset: [id], @@ -643,7 +693,7 @@ export class UpdateManager { const action = options?.action; const hasState = 'state' in stateAndParams; - const {items} = config; + 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)); @@ -690,7 +740,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