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