From 49130ccd924a8a5b8ec6d8ee50e1ba39fe3e8ee0 Mon Sep 17 00:00:00 2001 From: Taya Leutina Date: Mon, 11 Mar 2024 10:47:46 +0300 Subject: [PATCH] feat: support group in widget item (#118) (#120) * feat: support group in widget item * chore: add tests * fix: review issues --- src/shared/modules/__tests__/helpers.test.ts | 257 ++++++++++++++++++- src/shared/modules/helpers.ts | 198 ++++++++++---- src/shared/modules/state-and-params.ts | 214 +++++++++------ src/shared/modules/uniq-id.ts | 6 +- src/shared/types/config.ts | 16 ++ src/shared/types/state-and-params.ts | 6 +- src/typings/plugin.ts | 10 +- src/utils/__tests__/update-manager.test.ts | 207 ++++++++++++++- src/utils/update-manager.ts | 146 +++++++++-- 9 files changed, 912 insertions(+), 148 deletions(-) diff --git a/src/shared/modules/__tests__/helpers.test.ts b/src/shared/modules/__tests__/helpers.test.ts index 1d6c27b..3605a5d 100644 --- a/src/shared/modules/__tests__/helpers.test.ts +++ b/src/shared/modules/__tests__/helpers.test.ts @@ -1,6 +1,15 @@ -import {ConfigItem, StringParams, ItemsStateAndParams, ConfigItemData} from '../../types'; +import { + Config, + ConfigItem, + ConfigItemData, + ConfigItemWithGroup, + ItemsStateAndParams, + StringParams, +} from '../../types'; import {ACTION_PARAM_PREFIX, META_KEY} from '../../constants'; import { + addGroupToQueue, + addToQueue, formQueueData, hasActionParams, pickExceptActionParamsFromParams, @@ -8,6 +17,9 @@ import { } from '../helpers'; const DEFAULT_CONTROL_ID = 'controlId'; +const DEFAULT_CONTROL_ID_2 = 'controlId2'; +const DEFAULT_GROUP_ITEM_ID = 'groupItemId'; +const DEFAULT_GROUP_ITEM_ID_2 = 'groupItemId2'; const DEFAULT_WIDGET_ID = 'widgetId'; const DEFAULT_WIDGET_TAB_ID = 'widget_tabId'; const NAMESPACE = 'default'; @@ -17,6 +29,11 @@ type MockedControlItemArgs = { id?: string; }; +type MockedGroupControlItemArgs = MockedControlItemArgs & { + groupItemId?: string; + groupItemId2?: string; +}; + const getMockedControlItem = ({ id = DEFAULT_CONTROL_ID, defaults, @@ -28,11 +45,37 @@ const getMockedControlItem = ({ namespace: NAMESPACE, }); +const getMockedGroupControlItem = ({ + id = DEFAULT_CONTROL_ID, + groupItemId = DEFAULT_GROUP_ITEM_ID, + groupItemId2 = DEFAULT_GROUP_ITEM_ID_2, + defaults, +}: MockedGroupControlItemArgs): ConfigItemWithGroup => ({ + id, + data: { + group: [ + {id: groupItemId, namespace: NAMESPACE, defaults}, + {id: groupItemId2, namespace: NAMESPACE, defaults}, + ], + }, + type: 'group-control', + namespace: NAMESPACE, +}); + type MockedWidgetItemArgs = { id?: string; tabs?: ConfigItemData['tabs']; }; +const getMockedConfig = ({items}: {items: ConfigItem[]}): Config => ({ + items, + salt: '0.9021043992843898', + counter: 124, + layout: [], + aliases: {}, + connections: [], +}); + const stateAndParamsWithParamsOnly = { params: { paramName: 'param1', @@ -155,6 +198,63 @@ describe('modules.helpers', () => { ]).toEqual(formQueueData({items: [controlItem5], itemsStateAndParams})); }); + it('return correct queue data for control: group control', () => { + const itemsStateAndParams: ItemsStateAndParams = { + [DEFAULT_CONTROL_ID]: { + params: { + [DEFAULT_GROUP_ITEM_ID]: { + scale: 'd', + view: 'normal', + }, + }, + }, + [META_KEY]: { + queue: [{id: DEFAULT_CONTROL_ID, groupItemId: DEFAULT_GROUP_ITEM_ID}], + version: 2, + }, + }; + + const controlItem1 = getMockedGroupControlItem({ + id: 'control-1', + groupItemId: 'group-control-1', + }); + expect([]).toEqual(formQueueData({items: [controlItem1], itemsStateAndParams})); + + const controlItem2 = getMockedGroupControlItem({defaults: {number: 'one', size: 's'}}); + expect([ + { + id: DEFAULT_GROUP_ITEM_ID, + namespace: NAMESPACE, + params: {}, + }, + ]).toEqual(formQueueData({items: [controlItem2], itemsStateAndParams})); + + const controlItem4 = getMockedGroupControlItem({defaults: {scale: 'm', size: 's'}}); + expect([ + { + id: DEFAULT_GROUP_ITEM_ID, + namespace: NAMESPACE, + params: { + scale: 'd', + }, + }, + ]).toEqual(formQueueData({items: [controlItem4], itemsStateAndParams})); + + const controlItem5 = getMockedGroupControlItem({ + defaults: {scale: 'm', size: 's', view: 'contrast'}, + }); + expect([ + { + id: DEFAULT_GROUP_ITEM_ID, + namespace: NAMESPACE, + params: { + scale: 'd', + view: 'normal', + }, + }, + ]).toEqual(formQueueData({items: [controlItem5], itemsStateAndParams})); + }); + it('return correct queue data for widget: common', () => { const itemsStateAndParams: ItemsStateAndParams = { [DEFAULT_WIDGET_ID]: { @@ -282,10 +382,18 @@ describe('modules.helpers', () => { view: 'normal', }, }, + [DEFAULT_CONTROL_ID_2]: { + params: { + [DEFAULT_GROUP_ITEM_ID]: { + size: 'm', + }, + }, + }, [META_KEY]: { queue: [ {id: DEFAULT_CONTROL_ID}, {id: DEFAULT_WIDGET_ID, tabId: DEFAULT_WIDGET_TAB_ID}, + {id: DEFAULT_CONTROL_ID_2, groupItemId: DEFAULT_GROUP_ITEM_ID}, ], version: 2, }, @@ -301,20 +409,42 @@ describe('modules.helpers', () => { }); const control1 = getMockedControlItem({id: 'control-1'}); const control2 = getMockedControlItem({defaults: {scale: 'm', view: 'contrast'}}); + const groupControl1 = getMockedGroupControlItem({id: 'group-control-1'}); + const groupControl2 = getMockedGroupControlItem({ + id: DEFAULT_CONTROL_ID_2, + defaults: {size: 'xl', view: 'normal'}, + }); expect([ { id: DEFAULT_CONTROL_ID, namespace: NAMESPACE, - params: {scale: 'd'}, + params: { + scale: 'd', + }, }, { id: DEFAULT_WIDGET_ID, namespace: NAMESPACE, params: {view: 'normal'}, }, + { + id: DEFAULT_GROUP_ITEM_ID, + namespace: NAMESPACE, + params: { + size: 'm', + }, + }, ]).toEqual( formQueueData({ - items: [widgetItem2, widgetItem3, control1, widgetItem1, control2], + items: [ + widgetItem2, + widgetItem3, + control1, + widgetItem1, + control2, + groupControl1, + groupControl2, + ], itemsStateAndParams, }), ); @@ -364,4 +494,125 @@ describe('modules.helpers', () => { ); }); }); + + describe('queue helpers', () => { + it('add common control to the end of queue', () => { + const itemsStateAndParams: ItemsStateAndParams = { + [META_KEY]: { + queue: [ + {id: DEFAULT_CONTROL_ID}, + {id: DEFAULT_CONTROL_ID_2, groupItemId: DEFAULT_GROUP_ITEM_ID}, + ], + version: 2, + }, + }; + + const config = getMockedConfig({ + items: [ + getMockedControlItem({}), + getMockedGroupControlItem({id: DEFAULT_CONTROL_ID_2}), + ], + }); + + expect({ + queue: [ + {id: DEFAULT_CONTROL_ID_2, groupItemId: DEFAULT_GROUP_ITEM_ID}, + {id: DEFAULT_CONTROL_ID}, + ], + version: 2, + }).toEqual(addToQueue({id: DEFAULT_CONTROL_ID, config, itemsStateAndParams})); + }); + + it('remove unused items from queue after adding new group item: remove common items and group items', () => { + const itemsStateAndParams: ItemsStateAndParams = { + [META_KEY]: { + queue: [ + {id: 'control1'}, + {id: 'control2', tabId: 'tab'}, + {id: DEFAULT_CONTROL_ID_2, groupItemId: DEFAULT_GROUP_ITEM_ID_2}, + {id: DEFAULT_CONTROL_ID}, + {id: DEFAULT_CONTROL_ID_2, groupItemId: 'group-item-1'}, + ], + version: 2, + }, + }; + + const config = getMockedConfig({ + items: [ + getMockedControlItem({}), + getMockedGroupControlItem({id: DEFAULT_CONTROL_ID_2}), + ], + }); + expect({ + queue: [ + {id: DEFAULT_CONTROL_ID_2, groupItemId: DEFAULT_GROUP_ITEM_ID_2}, + {id: DEFAULT_CONTROL_ID}, + {id: DEFAULT_CONTROL_ID_2, groupItemId: DEFAULT_GROUP_ITEM_ID}, + ], + version: 2, + }).toEqual( + addGroupToQueue({ + id: DEFAULT_CONTROL_ID_2, + groupItemIds: [DEFAULT_GROUP_ITEM_ID], + config, + itemsStateAndParams, + }), + ); + }); + + it('remove unused items from queue after adding new item: remove common items and item with group', () => { + const itemsStateAndParams: ItemsStateAndParams = { + [META_KEY]: { + queue: [ + {id: 'control1'}, + {id: 'control2', tabId: 'a3'}, + {id: DEFAULT_CONTROL_ID_2, groupItemId: DEFAULT_GROUP_ITEM_ID_2}, + {id: DEFAULT_CONTROL_ID_2, groupItemId: DEFAULT_GROUP_ITEM_ID}, + ], + version: 2, + }, + }; + + const config = getMockedConfig({ + items: [getMockedControlItem({})], + }); + expect({ + queue: [{id: DEFAULT_CONTROL_ID}], + version: 2, + }).toEqual( + addToQueue({ + id: DEFAULT_CONTROL_ID, + config, + itemsStateAndParams, + }), + ); + }); + + it('add group control to queue', () => { + const itemsStateAndParams: ItemsStateAndParams = { + [META_KEY]: { + queue: [{id: DEFAULT_CONTROL_ID_2, groupItemId: DEFAULT_GROUP_ITEM_ID_2}], + version: 2, + }, + }; + + const config = getMockedConfig({ + items: [getMockedGroupControlItem({})], + }); + expect({ + queue: [ + {id: DEFAULT_CONTROL_ID_2, groupItemId: DEFAULT_GROUP_ITEM_ID}, + {id: DEFAULT_CONTROL_ID_2, groupItemId: DEFAULT_GROUP_ITEM_ID_2}, + ], + version: 2, + }).toEqual( + addGroupToQueue({ + id: DEFAULT_CONTROL_ID_2, + groupItemIds: [DEFAULT_GROUP_ITEM_ID, DEFAULT_GROUP_ITEM_ID_2], + config, + itemsStateAndParams, + }), + ); + }); + }); }); diff --git a/src/shared/modules/helpers.ts b/src/shared/modules/helpers.ts index 84306e1..95ebb99 100644 --- a/src/shared/modules/helpers.ts +++ b/src/shared/modules/helpers.ts @@ -3,20 +3,22 @@ import get from 'lodash/get'; import invert from 'lodash/invert'; import isEmpty from 'lodash/isEmpty'; import pick from 'lodash/pick'; -import {META_KEY, CURRENT_VERSION, ACTION_PARAM_PREFIX} from '../constants'; +import {ACTION_PARAM_PREFIX, CURRENT_VERSION, META_KEY} from '../constants'; import { - PluginBase, + Config, + ConfigAliases, + ConfigConnection, ConfigItem, + ConfigItemGroup, + ConfigItemWithGroup, + ConfigItemWithTabs, + ItemStateAndParams, ItemsStateAndParams, ItemsStateAndParamsBase, + PluginBase, + QueueItem, StateAndParamsMetaData, StringParams, - ConfigConnection, - ConfigAliases, - ConfigItemWithTabs, - Config, - QueueItem, - ItemStateAndParams, } from '../types'; function getNormalizedPlugins(plugins: PluginBase[]) { @@ -62,14 +64,22 @@ export function getCurrentVersion(itemsStateAndParams: ItemsStateAndParams): num return meta.version; } -function nonNullable(value: T): value is NonNullable { - return value !== null && value !== undefined; +function isConfigData( + item: Pick | ConfigItemGroup, +): item is Pick { + return 'data' in item && 'type' in item; } export function isItemWithTabs( - item: Pick, + item: Pick | ConfigItemGroup, ): item is Pick { - return Array.isArray(item?.data?.tabs); + return isConfigData(item) && Array.isArray(item?.data?.tabs); +} + +export function isItemWithGroup( + item: Pick, +): item is Pick { + return Array.isArray(item?.data?.group); } export type FormedQueueData = { @@ -88,49 +98,86 @@ export function formQueueData({ }): FormedQueueData[] { const queue = getItemsStateAndParamsMeta(itemsStateAndParams)?.queue || []; const keyById = keyBy(items, 'id'); - return queue - .map((queueItem) => { - const {id: queueId, tabId} = queueItem; - const item = keyById[queueId]; - if (!item) { - return null; - } - let itemDefaultParams: StringParams; - if (isItemWithTabs(item)) { - if (!tabId || resolveItemInnerId({item, itemsStateAndParams}) !== tabId) { - return null; - } - itemDefaultParams = - item.data.tabs.find((tabData) => tabData.id === tabId)?.params || {}; - } else { - itemDefaultParams = item.defaults || {}; - } + return queue.reduce((queueArray: FormedQueueData[], queueItem: QueueItem) => { + const {id: queueId, tabId, groupItemId} = queueItem; + const item = keyById[queueId]; + const isGroup = isItemWithGroup(item); + if (!item || (isGroup && !groupItemId)) { + return queueArray; + } - const itemQueueParams: StringParams = get(itemsStateAndParams, [item.id, 'params'], {}); - const filteredParamsByDefaults = pick(itemQueueParams, Object.keys(itemDefaultParams)); + if (isGroup && groupItemId) { + const itemQueueParams: Record = get( + itemsStateAndParams, + [item.id, 'params'], + {}, + ); + + const groupItem = item.data.group.find(({id}) => id === groupItemId); + if (!groupItem) { + return queueArray; + } + const groupItemQueueParams = itemQueueParams[groupItemId]; + const filteredParamsByDefaults = pick( + groupItemQueueParams, + Object.keys(groupItem.defaults || {}), + ); /** * merging filtered params and filtered actionParams with prefixes */ const params = { ...filteredParamsByDefaults, - ...(pickActionParamsFromParams(itemQueueParams, true) || {}), + ...(pickActionParamsFromParams(groupItemQueueParams, true) || {}), }; - return { - id: item.id, - namespace: item.namespace, + queueArray.push({ + id: groupItem.id, + namespace: groupItem.namespace, params, - }; - }) - .filter(nonNullable); + }); + + return queueArray; + } + + const itemQueueParams: StringParams = get(itemsStateAndParams, [item.id, 'params'], {}); + + let itemDefaultParams: StringParams; + if (isItemWithTabs(item)) { + if (!tabId || resolveItemInnerId({item, itemsStateAndParams}) !== tabId) { + return queueArray; + } + itemDefaultParams = + item.data.tabs.find((tabData) => tabData.id === tabId)?.params || {}; + } else { + itemDefaultParams = item.defaults || {}; + } + + const filteredParamsByDefaults = pick(itemQueueParams, Object.keys(itemDefaultParams)); + + /** + * merging filtered params and filtered actionParams with prefixes + */ + const params = { + ...filteredParamsByDefaults, + ...(pickActionParamsFromParams(itemQueueParams, true) || {}), + }; + + queueArray.push({ + id: item.id, + namespace: item.namespace, + params, + }); + + return queueArray; + }, []); } export function resolveItemInnerId({ item, itemsStateAndParams, }: { - item: ConfigItem; + item: Pick; itemsStateAndParams: ItemsStateAndParams; }): string { const {id} = item; @@ -152,7 +199,7 @@ export function getMapItemsIgnores({ itemsStateAndParams, isFirstVersion, }: { - items: ConfigItem[]; + items: (ConfigItem | ConfigItemGroup)[]; ignores: ConfigConnection[]; itemsStateAndParams: ItemsStateAndParams; isFirstVersion: boolean; @@ -231,10 +278,31 @@ export function getInitialItemsStateAndParamsMeta(): StateAndParamsMetaData { interface ChangeQueueArg { id: string; tabId?: string; + groupItemId?: string; + config: Config; + itemsStateAndParams: ItemsStateAndParams; +} + +interface ChangeQueueGroupArg { + id: string; + groupItemIds: string[]; config: Config; itemsStateAndParams: ItemsStateAndParams; } +function getActualItemsIds(items: ConfigItem[]) { + return items.reduce((ids: string[], item) => { + if (isItemWithGroup(item)) { + item.data.group.forEach((groupItem) => { + ids.push(groupItem.id); + }); + } + + ids.push(item.id); + return ids; + }, []); +} + export function addToQueue({ id, tabId, @@ -249,17 +317,52 @@ export function addToQueue({ if (!meta) { return {queue: [queueItem], version: CURRENT_VERSION}; } - const {items} = config; - const actualIds = items.map((item) => item.id); + const actualIds = getActualItemsIds(config.items); const metaQueue = meta.queue || []; + const notCurrent = (item: QueueItem) => { + if (item.groupItemId) { + return actualIds.includes(item.groupItemId); + } + return item.id !== id; + }; return { queue: metaQueue - .filter((item) => actualIds.includes(item.id) && item.id !== id) + .filter((item) => actualIds.includes(item.id) && notCurrent(item)) .concat(queueItem), version: meta.version || CURRENT_VERSION, }; } +export function addGroupToQueue({ + id, + groupItemIds, + config, + itemsStateAndParams, +}: ChangeQueueGroupArg): StateAndParamsMetaData { + const queueItems: QueueItem[] = groupItemIds.map((groupItemId) => ({ + id, + groupItemId, + })); + const meta = getItemsStateAndParamsMeta(itemsStateAndParams); + if (!meta) { + return {queue: queueItems, version: CURRENT_VERSION}; + } + const actualIds = getActualItemsIds(config.items); + const metaQueue = meta.queue || []; + const notCurrent = (item: QueueItem) => { + if (item.groupItemId) { + return actualIds.includes(item.groupItemId) && !groupItemIds.includes(item.groupItemId); + } + return true; + }; + return { + queue: metaQueue + .filter((item) => actualIds.includes(item.id) && notCurrent(item)) + .concat(queueItems), + version: meta.version || CURRENT_VERSION, + }; +} + export function deleteFromQueue(data: ChangeQueueArg): StateAndParamsMetaData { const meta = addToQueue(data); return { @@ -333,8 +436,13 @@ export function transformParamsToActionParams(params: ItemStateAndParams['params * check if object contains actionParams * @param conf */ -export function hasActionParam(conf?: StringParams): boolean { - return Object.keys(conf || {}).some((key) => key.startsWith(ACTION_PARAM_PREFIX)); +export function hasActionParam(conf?: StringParams | Record): boolean { + return Object.keys(conf || {}).some((key) => { + if (conf && typeof conf[key] === 'object' && !Array.isArray(conf[key])) { + return Object.keys(conf[key]).some((subkey) => subkey.startsWith(ACTION_PARAM_PREFIX)); + } + return key.startsWith(ACTION_PARAM_PREFIX); + }); } /** diff --git a/src/shared/modules/state-and-params.ts b/src/shared/modules/state-and-params.ts index f78bfb1..e3de14c 100644 --- a/src/shared/modules/state-and-params.ts +++ b/src/shared/modules/state-and-params.ts @@ -1,29 +1,31 @@ import groupBy from 'lodash/groupBy'; import {META_KEY} from '../constants'; import { - GlobalParams, Config, - ItemsStateAndParams, - PluginBase, ConfigItem, - StringParams, + ConfigItemDataWithTabs, + ConfigItemGroup, + GlobalParams, ItemState, + ItemStateAndParams, + ItemsStateAndParams, ItemsStateAndParamsBase, + PluginBase, StateAndParamsMetaData, - ItemStateAndParams, - ConfigItemDataWithTabs, + StringParams, } from '../types'; import { - prerenderItems, - formQueueData, FormedQueueData, + formQueueData, + getCurrentVersion, getMapItemsIgnores, + hasActionParam, + isItemWithGroup, + isItemWithTabs, mergeParamsWithAliases, - getCurrentVersion, pickActionParamsFromParams, - hasActionParam, pickExceptActionParamsFromParams, - isItemWithTabs, + prerenderItems, resolveItemInnerId, } from './helpers'; @@ -35,7 +37,93 @@ export interface GetItemsParamsArg { plugins: PluginBase[]; } -type GetItemsParamsReturn = Record; +type GetItemsParamsReturn = Record>; + +function getItemParams({ + item, + itemsStateAndParams, + mapItemsIgnores, + itemsWithDefaultsByNamespace, + getMergedParams, + defaultGlobalParams, + globalParams, + isFirstVersion, + queueData, +}: { + item: ConfigItem | ConfigItemGroup; + itemsStateAndParams: ItemsStateAndParams; + mapItemsIgnores: Record; + itemsWithDefaultsByNamespace: Record; + getMergedParams: (params: StringParams, actionParams?: StringParams) => StringParams; + defaultGlobalParams: StringParams; + globalParams: GlobalParams; + isFirstVersion: boolean; + queueData: FormedQueueData[]; +}) { + const {id, namespace} = item; + + let defaultWidgetParams: StringParams | Record = {}; + if (isItemWithTabs(item)) { + const currentWidgetTabId = resolveItemInnerId({item, itemsStateAndParams}); + const itemTabs: ConfigItemDataWithTabs['tabs'] = item.data.tabs; + defaultWidgetParams = + itemTabs.find((tabItem) => tabItem?.id === currentWidgetTabId)?.params || {}; + } else { + defaultWidgetParams = item.defaults || {}; + } + + const itemIgnores = mapItemsIgnores[id]; + + const affectingItemsWithDefaults = itemsWithDefaultsByNamespace[namespace].filter( + (itemWithDefaults) => !itemIgnores.includes(itemWithDefaults.id), + ); + + let itemParams: StringParams = Object.assign( + {}, + getMergedParams(defaultGlobalParams), + // default parameters to begin with + affectingItemsWithDefaults.reduceRight((defaultParams: StringParams, itemWithDefaults) => { + return { + ...defaultParams, + ...getMergedParams(itemWithDefaults.defaults || {}), + }; + }, {}), + getMergedParams(globalParams), + ); + if (isFirstVersion) { + itemParams = Object.assign( + itemParams, + (itemsStateAndParams as ItemsStateAndParamsBase)?.[id]?.params || {}, + ); + } else { + // params according to queue of its applying + let queueDataItemsParams: StringParams = {}; + for (const data of queueData) { + if (data.namespace !== namespace || itemIgnores.includes(data.id)) { + continue; + } + + let actionParams; + let params = data.params; + const needAliasesForActionParams = data.id !== id && hasActionParam(data.params); + if (needAliasesForActionParams) { + actionParams = pickActionParamsFromParams(data.params); + params = pickExceptActionParamsFromParams(data.params); + } + + const mergedParams = getMergedParams(params, actionParams); + + queueDataItemsParams = { + ...queueDataItemsParams, + ...mergedParams, + }; + } + + itemParams = Object.assign(itemParams, queueDataItemsParams); + } + + return {...defaultWidgetParams, ...itemParams}; +} export function getItemsParams({ defaultGlobalParams = {}, @@ -47,18 +135,32 @@ export function getItemsParams({ const {aliases, connections} = config; const items = prerenderItems({items: config.items, plugins}); const isFirstVersion = getCurrentVersion(itemsStateAndParams) === 1; + + const allItems = items.reduce((paramsItems: (ConfigItem | ConfigItemGroup)[], item) => { + if (isItemWithGroup(item)) { + item.data.group.forEach((groupItem) => { + paramsItems.push(groupItem); + }); + + return paramsItems; + } + + paramsItems.push(item); + return paramsItems; + }, []); + const queueData: FormedQueueData[] = isFirstVersion ? [] : formQueueData({items, itemsStateAndParams}); // to consider other kind types in future (not only ignore) const mapItemsIgnores = getMapItemsIgnores({ - items, + items: allItems, ignores: connections.filter(({kind}) => kind === 'ignore'), itemsStateAndParams, isFirstVersion, }); - const groupByNamespace = groupBy(items, 'namespace'); + const groupByNamespace = groupBy(allItems, 'namespace'); const itemsWithDefaultsByNamespace = Object.keys(groupByNamespace).reduce((acc, namespace) => { return { ...acc, @@ -66,78 +168,46 @@ export function getItemsParams({ // but make a decision about there's order first [namespace]: groupByNamespace[namespace].filter((item) => item.defaults), }; - }, {} as Record); + }, {} as Record); - return items.reduce((itemsParams: Record, item) => { + return items.reduce((itemsParams: GetItemsParamsReturn, item: ConfigItem) => { const {id, namespace} = item; - let defaultWidgetParams: StringParams = {}; - if (isItemWithTabs(item)) { - const currentWidgetTabId = resolveItemInnerId({item, itemsStateAndParams}); - const itemTabs: ConfigItemDataWithTabs['tabs'] = item.data.tabs; - defaultWidgetParams = - itemTabs.find((tabItem) => tabItem?.id === currentWidgetTabId)?.params || {}; - } else { - defaultWidgetParams = item.defaults || {}; - } - const getMergedParams = (params: StringParams, actionParams?: StringParams) => mergeParamsWithAliases({aliases, namespace, params: params || {}, actionParams}); - const itemIgnores = mapItemsIgnores[id]; - const affectingItemsWithDefaults = itemsWithDefaultsByNamespace[namespace].filter( - (itemWithDefaults) => !itemIgnores.includes(itemWithDefaults.id), - ); - let itemParams: StringParams = Object.assign( - {}, - getMergedParams(defaultGlobalParams), - // default parameters to begin with - affectingItemsWithDefaults.reduceRight( - (defaultParams: StringParams, itemWithDefaults) => { - return { - ...defaultParams, - ...getMergedParams(itemWithDefaults.defaults || {}), - }; + const paramsOptions = { + itemsStateAndParams, + mapItemsIgnores, + itemsWithDefaultsByNamespace, + getMergedParams, + defaultGlobalParams, + globalParams, + isFirstVersion, + queueData, + }; + + if (isItemWithGroup(item)) { + const groupParams = item.data.group.reduce( + (groupItemParams: Record, groupItem) => { + groupItemParams[groupItem.id] = getItemParams({ + item: groupItem, + ...paramsOptions, + }); + return groupItemParams; }, {}, - ), - getMergedParams(globalParams), - ); - if (isFirstVersion) { - itemParams = Object.assign( - itemParams, - (itemsStateAndParams as ItemsStateAndParamsBase)?.[id]?.params || {}, ); - } else { - // params according to queue of its applying - let queueDataItemsParams: StringParams = {}; - for (const data of Object.values(queueData)) { - if (data.namespace !== namespace || itemIgnores.includes(data.id)) { - continue; - } - - let actionParams; - let params = data.params; - const needAliasesForActionParams = data.id !== id && hasActionParam(data.params); - if (needAliasesForActionParams) { - actionParams = pickActionParamsFromParams(data.params); - params = pickExceptActionParamsFromParams(data.params); - } - - const mergedParams = getMergedParams(params, actionParams); - - queueDataItemsParams = { - ...queueDataItemsParams, - ...mergedParams, - }; - } - itemParams = Object.assign(itemParams, queueDataItemsParams); + return {...itemsParams, [id]: groupParams}; } return { ...itemsParams, - [id]: {...defaultWidgetParams, ...itemParams}, + [id]: getItemParams({ + item, + ...paramsOptions, + }), }; }, {}); } diff --git a/src/shared/modules/uniq-id.ts b/src/shared/modules/uniq-id.ts index e4c66c7..e07d15b 100644 --- a/src/shared/modules/uniq-id.ts +++ b/src/shared/modules/uniq-id.ts @@ -1,6 +1,7 @@ import Hashids from 'hashids'; import type {Config} from '../types'; -import {isItemWithTabs} from './helpers'; + +import {isItemWithGroup, isItemWithTabs} from './helpers'; export function extractIdsFromConfig(config: Config): string[] { const ids: string[] = []; @@ -14,6 +15,9 @@ export function extractIdsFromConfig(config: Config): string[] { if (isItemWithTabs(item)) { item.data.tabs.forEach((tabItem) => ids.push(tabItem.id)); } + if (isItemWithGroup(item)) { + item.data.group.forEach((groupItem) => ids.push(groupItem.id)); + } }); connections.forEach(({from, to}) => ids.push(from, to)); layout.forEach(({i}) => ids.push(i)); diff --git a/src/shared/types/config.ts b/src/shared/types/config.ts index 372160e..6c06985 100644 --- a/src/shared/types/config.ts +++ b/src/shared/types/config.ts @@ -8,6 +8,13 @@ export interface ConfigLayout { y: number; } +export type ConfigItemGroup = { + id: string; + defaults?: StringParams; + namespace: string; + [key: string]: unknown; +}; + export interface ConfigItemData { _editActive?: boolean; tabs?: { @@ -16,6 +23,7 @@ export interface ConfigItemData { params?: StringParams; [key: string]: unknown; }[]; + group?: ConfigItemGroup[]; [key: string]: unknown; } @@ -23,6 +31,10 @@ export interface ConfigItemDataWithTabs extends Omit { tabs: NonNullable; } +export interface ConfigItemDataWithGroup extends Omit { + group: NonNullable; +} + export interface ConfigItem { id: string; data: ConfigItemData; @@ -37,6 +49,10 @@ export interface ConfigItemWithTabs extends Omit { data: ConfigItemDataWithTabs; } +export interface ConfigItemWithGroup extends Omit { + data: ConfigItemDataWithGroup; +} + export interface ConfigAliases { [namespace: string]: string[][]; // в массивах имена параметров } diff --git a/src/shared/types/state-and-params.ts b/src/shared/types/state-and-params.ts index c6cef49..22a74ea 100644 --- a/src/shared/types/state-and-params.ts +++ b/src/shared/types/state-and-params.ts @@ -8,6 +8,7 @@ export interface ItemState { export interface QueueItem { id: string; tabId?: string; + groupItemId?: string; } export type StateAndParamsMetaData = { @@ -20,12 +21,13 @@ export type StateAndParamsMeta = { }; export type ItemStateAndParams = { - params?: StringParams; + params?: StringParams | Record; state?: ItemState; }; export type ItemStateAndParamsChangeOptions = { - action: 'setParams' | 'removeItem'; + action?: 'setParams' | 'removeItem'; + groupItemIds?: string[]; }; export type ItemsStateAndParamsBase = Record; diff --git a/src/typings/plugin.ts b/src/typings/plugin.ts index 8d5e88f..e000fb2 100644 --- a/src/typings/plugin.ts +++ b/src/typings/plugin.ts @@ -1,20 +1,20 @@ import React from 'react'; import {ContextProps, SettingsProps, WidgetLayout} from './common'; import { - StringParams, ConfigItem, ItemState, ItemStateAndParams, - PluginBase, ItemStateAndParamsChangeOptions, + PluginBase, + StringParams, } from '../shared'; import type {ReactGridLayoutProps} from 'react-grid-layout'; -export interface PluginWidgetProps { +export interface PluginWidgetProps { id: string; editMode: boolean; - params: StringParams; + params: T; state: ItemState; onStateAndParamsChange: ( stateAndParams: ItemStateAndParams, @@ -47,7 +47,7 @@ export interface PluginDefaultLayout { maxH?: number; } -export interface Plugin

extends PluginBase { +export interface Plugin

= any, T = StringParams> extends PluginBase { defaultLayout?: PluginDefaultLayout; renderer: (props: P, forwardedRef: React.RefObject) => React.ReactNode; } diff --git a/src/utils/__tests__/update-manager.test.ts b/src/utils/__tests__/update-manager.test.ts index f2daabb..041f332 100644 --- a/src/utils/__tests__/update-manager.test.ts +++ b/src/utils/__tests__/update-manager.test.ts @@ -117,6 +117,61 @@ const config: Config = { }, namespace: 'default', }, + { + id: 'qY', + data: { + group: [ + { + id: '9YN', + title: 'Category', + width: '', + source: { + datasetId: 'rfvnhj345gugf', + fieldType: 'string', + elementType: 'select', + datasetFieldId: 'ab089', + datasetFieldType: 'DIMENSION', + }, + defaults: { + ab089: '', + }, + sourceType: 'dataset', + placementMode: 'auto', + namespace: 'default', + }, + { + id: 'qav', + title: 'Селектор 1', + width: '', + source: { + fieldName: 'sds', + elementType: 'select', + acceptableValues: [ + { + title: '2', + value: '2', + }, + { + title: '34', + value: '34', + }, + ], + }, + defaults: { + sds: '', + }, + sourceType: 'manual', + placementMode: 'auto', + namespace: 'default', + }, + ], + autoHeight: true, + buttonApply: false, + buttonReset: false, + }, + type: 'group_control', + namespace: 'default', + }, ], layout: [ {h: 4, i: 'al', w: 4, x: 8, y: 0}, @@ -124,6 +179,7 @@ const config: Config = { {h: 2, i: 'L5', w: 8, x: 0, y: 2}, {h: 2, i: 'C8', w: 8, x: 10, y: 30}, {h: 2, i: 'lko', w: 8, x: 0, y: 12}, + {h: 2, i: 'qY', w: 8, x: 0, y: 24}, ], aliases: {}, connections: [], @@ -135,7 +191,7 @@ beforeEach(() => { describe('UpdateManager', () => { describe('changeStateAndParams', () => { - it('control adds params to empty itemsStateAndParams', () => { + it('control adds params to empty itemsStateAndParams: common control', () => { expect( UpdateManager.changeStateAndParams({ id: 'L5', @@ -157,6 +213,33 @@ describe('UpdateManager', () => { }); }); + it('control adds params to empty itemsStateAndParams: group control', () => { + expect( + UpdateManager.changeStateAndParams({ + id: 'qY', + config, + itemsStateAndParams: {}, + stateAndParams: { + params: { + ab089: ['red'], + }, + }, + options: { + groupItemIds: ['9YN'], + }, + }), + ).toEqual({ + qY: { + params: { + '9YN': { + ab089: ['red'], + }, + }, + }, + __meta__: {queue: [{id: 'qY', groupItemId: '9YN'}], version: 2}, + }); + }); + it('state changes only do not push into the queue', () => { expect( UpdateManager.changeStateAndParams({ @@ -168,7 +251,17 @@ describe('UpdateManager', () => { d079: ['Russia'], }, }, - __meta__: {queue: [{id: 'L5'}, {id: 'Unk'}], version: 2}, + qY: { + params: { + '9YN': { + ab089: ['red'], + }, + }, + }, + __meta__: { + queue: [{id: 'L5'}, {id: 'qY', groupItemId: '9YN'}, {id: 'Unk'}], + version: 2, + }, }, stateAndParams: { state: {tabId: 'Pf'}, @@ -180,12 +273,19 @@ describe('UpdateManager', () => { d079: ['Russia'], }, }, + qY: { + params: { + '9YN': { + ab089: ['red'], + }, + }, + }, al: { state: { tabId: 'Pf', }, }, - __meta__: {queue: [{id: 'L5'}], version: 2}, + __meta__: {queue: [{id: 'L5'}, {id: 'qY', groupItemId: '9YN'}], version: 2}, }); }); @@ -219,7 +319,7 @@ describe('UpdateManager', () => { }); }); - it('control changes rearrange position in the queue', () => { + it('control changes rearrange position in the queue: common control', () => { expect( UpdateManager.changeStateAndParams({ id: 'L5', @@ -280,6 +380,77 @@ describe('UpdateManager', () => { }); }); + it('control changes rearrange position in the queue: group control', () => { + expect( + UpdateManager.changeStateAndParams({ + id: 'qY', + config, + itemsStateAndParams: { + al: { + params: { + Country: 'USA', + }, + }, + qY: { + params: { + qav: { + sds: ['yellow'], + }, + '9YN': { + ab089: ['red'], + }, + }, + }, + __meta__: { + queue: [ + {id: 'qY', groupItemId: 'qav'}, + {id: 'qY', groupItemId: '9YN'}, + {id: 'al'}, + ], + version: 2, + }, + }, + stateAndParams: { + params: { + qav: { + sds: ['violet'], + }, + '9YN': { + ab089: ['purple'], + }, + }, + }, + options: { + groupItemIds: ['qav'], + }, + }), + ).toEqual({ + al: { + params: { + Country: 'USA', + }, + }, + qY: { + params: { + qav: { + sds: ['violet'], + }, + '9YN': { + ab089: ['red'], + }, + }, + }, + __meta__: { + queue: [ + {id: 'qY', groupItemId: '9YN'}, + {id: 'al'}, + {id: 'qY', groupItemId: 'qav'}, + ], + version: 2, + }, + }); + }); + it('control params should merge', () => { expect( UpdateManager.changeStateAndParams({ @@ -326,7 +497,7 @@ describe('UpdateManager', () => { }); }); - it('control cannot set params not from defaults', () => { + it('control cannot set params not from defaults: common control', () => { expect( UpdateManager.changeStateAndParams({ id: 'lko', @@ -349,6 +520,32 @@ describe('UpdateManager', () => { }); }); + it('control cannot set params not from defaults: group control', () => { + expect( + UpdateManager.changeStateAndParams({ + id: 'qY', + config, + itemsStateAndParams: {}, + stateAndParams: { + params: { + sds: ['value'], + UnknownParam: ['Value'], + }, + }, + options: {groupItemIds: ['qav']}, + }), + ).toEqual({ + qY: { + params: { + qav: { + sds: ['value'], + }, + }, + }, + __meta__: {queue: [{id: 'qY', groupItemId: 'qav'}], version: 2}, + }); + }); + it('filtering_charts_table can set action params not from defaults', () => { expect( UpdateManager.changeStateAndParams({ diff --git a/src/utils/update-manager.ts b/src/utils/update-manager.ts index fc4a92c..750168e 100644 --- a/src/utils/update-manager.ts +++ b/src/utils/update-manager.ts @@ -1,27 +1,32 @@ -import update, {extend, Spec, CustomCommands} from 'immutability-helper'; +import update, {CustomCommands, Spec, extend} from 'immutability-helper'; import omit from 'lodash/omit'; import pick from 'lodash/pick'; import { - mergeParamsWithAliases, - isItemWithTabs, Config, ConfigItem, - ItemsStateAndParams, + ConfigItemGroup, + ConfigItemWithGroup, ItemStateAndParams, - ItemsStateAndParamsBase, ItemStateAndParamsChangeOptions, - StringParams, - getCurrentVersion, + ItemsStateAndParams, + ItemsStateAndParamsBase, META_KEY, - deleteFromQueue, + StateAndParamsMetaData, + StringParams, + addGroupToQueue, addToQueue, + deleteFromQueue, + getCurrentVersion, getInitialItemsStateAndParamsMeta, - resolveItemInnerId, getItemsStateAndParamsMeta, + isItemWithGroup, + isItemWithTabs, + mergeParamsWithAliases, pickActionParamsFromParams, + resolveItemInnerId, transformParamsToActionParams, } from '../shared'; -import {AddConfigItem, WidgetLayout, SetItemOptions} from '../typings'; +import {AddConfigItem, SetItemOptions, WidgetLayout} from '../typings'; import {RegisterManagerPluginLayout} from './register-manager'; import {DEFAULT_NAMESPACE} from '../constants'; import {getNewId} from './get-new-id'; @@ -103,7 +108,7 @@ function removeItemVersion1({id, config, itemsStateAndParams}: RemoveItemArg) { } function getAllowableChangedParams( - item: ConfigItem, + item: ConfigItem | ConfigItemGroup, stateAndParams: ItemStateAndParams, itemsStateAndParams: ItemsStateAndParams, paramsSettings?: { @@ -135,7 +140,13 @@ function getAllowableChangedParams( } allowedParams = pick(stateParamsConf, Object.keys(tab?.params || {})) as StringParams; } else { - allowedParams = pick(stateParamsConf, Object.keys(item.defaults || {})) as StringParams; + // check if structure is StringParams or Record + const paramsConf = + typeof stateParamsConf?.[item.id] === 'object' && + !Array.isArray(stateParamsConf?.[item.id]) + ? stateParamsConf[item.id] + : stateParamsConf; + allowedParams = pick(paramsConf, Object.keys(item.defaults || {})) as StringParams; } if (Object.keys(allowedParams || {}).length !== Object.keys(stateParamsConf || {}).length) { console.warn('Параметры, которых нет в defaults, будут проигнорированы!'); @@ -174,6 +185,7 @@ function changeStateAndParamsVersion1({ const ignoresInitiatorIds = config.connections .filter(({to}) => to === initiatorId) .map(({from}) => from); + const updateIds = config.items .filter( (item) => @@ -244,9 +256,96 @@ function getNewItemData({item, config, counter: argsCounter, salt, options}: Get data = {...item.data, tabs}; } + + if (isItemWithGroup(item)) { + const group = item.data.group.map((groupItem) => { + if (groupItem.id) { + return groupItem; + } + + const newIdGroupData = getNewId({config, salt, counter, excludeIds}); + counter = newIdGroupData.counter; + excludeIds.push(newIdGroupData.id); + + return { + ...groupItem, + id: newIdGroupData.id, + namespace: groupItem.namespace || DEFAULT_NAMESPACE, + }; + }); + + data = {...item.data, group}; + } + return {data, counter, excludeIds}; } +function changeGroupParams({ + groupItemIds, + initiatorId, + initiatorItem, + itemsStateAndParams, + stateAndParams, + config, + unusedIds, +}: { + groupItemIds: string[]; + initiatorId: string; + initiatorItem: ConfigItemWithGroup; + config: Config; + itemsStateAndParams: ItemsStateAndParams; + stateAndParams: ItemStateAndParams; + unusedIds: string[]; +}) { + const meta: StateAndParamsMetaData = addGroupToQueue({ + id: initiatorId, + groupItemIds, + config, + itemsStateAndParams, + }); + const updatedItems: Record = {}; + const currentItemParams = (itemsStateAndParams as ItemsStateAndParamsBase)[initiatorId] + ?.params as Record | undefined; + + for (const groupItem of initiatorItem.data.group) { + if (groupItemIds.includes(groupItem.id)) { + const allowableParams = getAllowableChangedParams( + groupItem, + stateAndParams, + itemsStateAndParams, + ); + + const allowableActionParams = getAllowableChangedParams( + groupItem, + stateAndParams, + itemsStateAndParams, + {type: 'actionParams', returnPrefix: true}, + ); + + updatedItems[groupItem.id] = {...allowableParams, ...allowableActionParams}; + + continue; + } + + if (currentItemParams && currentItemParams[groupItem.id]) { + updatedItems[groupItem.id] = currentItemParams[groupItem.id]; + } + } + + const obj = { + $unset: unusedIds, + [initiatorId]: { + $auto: { + params: { + $set: updatedItems, + }, + }, + }, + [META_KEY]: {$set: meta}, + }; + return update(itemsStateAndParams, obj); +} + export class UpdateManager { static addItem({ item, @@ -317,9 +416,13 @@ export class UpdateManager { } const itemIndex = config.items.findIndex((item) => item.id === id); const item = config.items[itemIndex]; - const itemIds = isItemWithTabs(item) - ? [id].concat(item.data.tabs.map((tab) => tab.id)) - : [id]; + let itemIds = [id]; + if (isItemWithTabs(item)) { + itemIds = [id].concat(item.data.tabs.map((tab) => tab.id)); + } + if (isItemWithGroup(item)) { + itemIds = [id].concat(item.data.group.map((groupItem) => groupItem.id)); + } const connections = config.connections.filter( ({from, to}) => !itemIds.includes(from) && !itemIds.includes(to), ); @@ -382,6 +485,7 @@ export class UpdateManager { const newTabId: string | undefined = stateAndParams.state?.tabId; const isTabSwitched = isItemWithTabs(initiatorItem) && Boolean(newTabId); const currentMeta = getItemsStateAndParamsMeta(itemsStateAndParams); + const groupItemIds = options?.groupItemIds; if (action === 'removeItem') { return update(itemsStateAndParams, { @@ -398,6 +502,18 @@ export class UpdateManager { }); } + if (isItemWithGroup(initiatorItem) && groupItemIds) { + return changeGroupParams({ + initiatorId, + initiatorItem, + groupItemIds, + itemsStateAndParams, + stateAndParams, + config, + unusedIds, + }); + } + if ('params' in stateAndParams) { const allowableParams = getAllowableChangedParams( initiatorItem,