diff --git a/demo/js/esri-datasets.js b/demo/js/esri-datasets.js index 4f868cf12..6fd69fe33 100644 --- a/demo/js/esri-datasets.js +++ b/demo/js/esri-datasets.js @@ -12,6 +12,7 @@ import scaleBarPlugin from '/plugins/beta/scale-bar/src/index.js' import searchPlugin from '/plugins/search/src/index.js' import { transformGeocodeRequest, transformVtsRequest3857, setupEsriConfig } from './auth.js' import createInteractPlugin from '/plugins/interact/src/index.js' +import { siteBoundary } from './planning/siteBoundary.js' const nonFloodZoneLight = '#2b8cbe' const nonFloodZoneDark = '#7fcdbb' @@ -301,10 +302,9 @@ const surfaceWaterDatasetGenerator = ({id, tileName, sourceLayer, timeframe, aep if (depthsKey) { return [extentsDataset, depthDataset] } - // We only really need one of these with visibleWhen: { menu: {dataset: ['surfacewater'], depth: ['depthAll'] } }, + // We only need one depthsKey, so we only return it here if it isn't already defined depthsKey = { id: 'depths-key', - label: 'Surface water', groupId: 'surface-water-depth-in-millimetres', showInKey: true, visibleWhen: { menu: { dataset: ['surfacewater'], depth: ['depthAll'] } }, @@ -316,17 +316,7 @@ const surfaceWaterDatasetGenerator = ({id, tileName, sourceLayer, timeframe, aep } }) } - const extraDepthKeys = [] - for(let i = 3; i <= depthsKey.sublayers.length; i++) { - extraDepthKeys.push({ - ...depthsKey, - groupLabel: `${depthsKey.groupLabel} [${i}]`, - id: `${depthsKey.id}-${i}`, - sublayers: depthsKey.sublayers.slice(0, i).map((sublayer) => ({ ...sublayer, label: `${i}.${sublayer.label}`.replaceAll('0', '') })), - }) - } return [depthsKey, extentsDataset, depthDataset] - // return [...extraDepthKeys, extentsDataset, depthDataset] } const surfaceWaterExtentsKey = { @@ -616,6 +606,23 @@ const interactPlugin = createInteractPlugin({ interactionModes: ['placeMarker'], }) +const mapKeyPlugin = createMapKeyPlugin({ + groups: { + 'surface-water-depth-in-millimetres': { + groupLabel: 'Surface water depth in millimetres', + groupStyle: 'horizontal-ramp' + } + }, + manifest: { + panels: [{ + id: 'mapKey', + mobile: { slot: 'drawer', modal: false }, + tablet: { slot: 'left-top', width: '360px' }, + desktop: { slot: 'left-top', width: '360px' }, + }] + }, +}) + const interactiveMap = new InteractiveMap('map', { behaviour: 'mapOnly', mapProvider: esriProvider({ setupConfig: setupEsriConfig }), @@ -644,22 +651,7 @@ const interactiveMap = new InteractiveMap('map', { scaleBarPlugin({ units: 'metric' }), drawPlugin, framePlugin, - createMapKeyPlugin({ - groups: { - 'surface-water-depth-in-millimetres': { - groupLabel: 'Surface water depth in millimetres', - groupStyle: 'horizontal-ramp' - } - }, - manifest: { - panels: [{ - id: 'mapKey', - mobile: { slot: 'drawer', modal: false }, - tablet: { slot: 'left-top', width: '360px' }, - desktop: { slot: 'left-top', width: '360px' }, - }] - }, - }), + mapKeyPlugin, createMenuPlugin({ manifest: { panels: [{ @@ -703,6 +695,29 @@ interactiveMap.on('interact:markerchange', function (e) { }) }) +const siteBoundaryKeyDefinition = { + id: 'site-boundary', + label: 'Location boundary', + // groupLabel: 'Other features', + style: { + strokeWidth: 2, + fill: 'none', + stroke: { outdoor: '#D4351D', dark: '#ffffff' } + }, +} + +siteBoundary.onSetFeature = (feature) => { + if (feature) { + mapKeyPlugin.addSymbol(siteBoundaryKeyDefinition) + } else { + mapKeyPlugin.removeSymbol(siteBoundaryKeyDefinition) + } +} + + interactiveMap.on('map-key:ready', function () { + siteBoundary.onSetFeature(true) + }) + const onEditPolygon = (isEditing) => { // toggleKeyWhenEditing(isEditing) diff --git a/demo/js/planning/siteBoundary.js b/demo/js/planning/siteBoundary.js index 5e6c46505..25226488f 100644 --- a/demo/js/planning/siteBoundary.js +++ b/demo/js/planning/siteBoundary.js @@ -7,6 +7,7 @@ const FRAME_MAX_ZOOM = 22 export class SiteBoundary { constructor (id = 'boundary') { + this.onSetFeature = () => {} this._feature = null this._id = id this._state = SiteBoundary.EMPTY @@ -66,6 +67,7 @@ export class SiteBoundary { if (!feature?.geometry?.coordinates) { this._feature = null this.state = SiteBoundary.EMPTY + this.onSetFeature(null) return } // round the coordinates to 2 decimal places @@ -78,6 +80,7 @@ export class SiteBoundary { // set the feature and update the state this._feature = { ...feature, id, properties } this.state = SiteBoundary.COMPLETE + this.onSetFeature(this._feature) } diff --git a/plugins/datasets/src/registry/datasetRegistry.js b/plugins/datasets/src/registry/datasetRegistry.js index e543a37e0..d9d9de158 100644 --- a/plugins/datasets/src/registry/datasetRegistry.js +++ b/plugins/datasets/src/registry/datasetRegistry.js @@ -101,16 +101,16 @@ const datasetRegistry = { const groups = new Map() const getOrCreateGroup = (groupLabel, groupId) => { - if (groups.has(groupLabel)) { - return groups.get(groupLabel) + if (groups.has(groupId)) { + return groups.get(groupId) } const groupObject = { type: 'group', - id: groupId, // groupLabel.toLowerCase().replaceAll(/\s+/g, '-'), + id: groupId, groupLabel, keyDefinitions: [] } - groups.set(groupLabel, groupObject) + groups.set(groupId, groupObject) _items.push(groupObject) return groupObject } @@ -119,14 +119,14 @@ const datasetRegistry = { if (!dataset.keyVisibility) { return } - const isGroup = dataset.hasSublayers || dataset.groupLabel + const isGroup = dataset.hasSublayers || dataset.groupId || dataset.groupLabel if (!isGroup) { _items.push({ type: 'flat', id: dataset.id, keyDefinition: dataset.keyDefinition }) return } const groupId = dataset.groupId - const groupLabel = dataset.groupLabel || dataset.label + const groupLabel = dataset.groupLabel || groupId || dataset.label const groupObject = getOrCreateGroup(groupLabel, groupId) if (!dataset.hasSublayers) { groupObject.keyDefinitions.push(dataset.keyDefinition) diff --git a/plugins/map-key/src/api/addSymbol.js b/plugins/map-key/src/api/addSymbol.js new file mode 100644 index 000000000..c4a6749c4 --- /dev/null +++ b/plugins/map-key/src/api/addSymbol.js @@ -0,0 +1,3 @@ +export const addSymbol = ({ pluginState: { dispatch } }, keyDefinition) => { + dispatch({ type: 'ADD_KEY_SYMBOL', payload: keyDefinition }) +} diff --git a/plugins/map-key/src/api/addSymbol.test.js b/plugins/map-key/src/api/addSymbol.test.js new file mode 100644 index 000000000..3a618b71a --- /dev/null +++ b/plugins/map-key/src/api/addSymbol.test.js @@ -0,0 +1,14 @@ +import { addSymbol } from './addSymbol.js' + +describe('addSymbol', () => { + it('dispatches an ADD_KEY_SYMBOL action with the key definition', () => { + const dispatch = jest.fn() + + addSymbol({ pluginState: { dispatch } }, { id: 'map-key', groupLabel: 'Map key' }) + + expect(dispatch).toHaveBeenCalledWith({ + type: 'ADD_KEY_SYMBOL', + payload: { id: 'map-key', groupLabel: 'Map key' } + }) + }) +}) diff --git a/plugins/map-key/src/api/index.js b/plugins/map-key/src/api/index.js new file mode 100644 index 000000000..96180320a --- /dev/null +++ b/plugins/map-key/src/api/index.js @@ -0,0 +1,7 @@ +import { addSymbol } from './addSymbol.js' +import { removeSymbol } from './removeSymbol.js' + +export const api = { + addSymbol, + removeSymbol +} diff --git a/plugins/map-key/src/api/index.test.js b/plugins/map-key/src/api/index.test.js new file mode 100644 index 000000000..db9cd7ce8 --- /dev/null +++ b/plugins/map-key/src/api/index.test.js @@ -0,0 +1,13 @@ +import { api } from './index.js' +import { addSymbol } from './addSymbol.js' +import { removeSymbol } from './removeSymbol.js' + +describe('api/index', () => { + it('exports the addSymbol API', () => { + expect(api.addSymbol).toBe(addSymbol) + }) + + it('exports the removeSymbol API', () => { + expect(api.removeSymbol).toBe(removeSymbol) + }) +}) diff --git a/plugins/map-key/src/api/removeSymbol.js b/plugins/map-key/src/api/removeSymbol.js new file mode 100644 index 000000000..7c4fe284e --- /dev/null +++ b/plugins/map-key/src/api/removeSymbol.js @@ -0,0 +1,3 @@ +export const removeSymbol = ({ pluginState: { dispatch } }, keyDefinition) => { + dispatch({ type: 'REMOVE_KEY_SYMBOL', payload: keyDefinition }) +} diff --git a/plugins/map-key/src/api/removeSymbol.test.js b/plugins/map-key/src/api/removeSymbol.test.js new file mode 100644 index 000000000..2601ebb1e --- /dev/null +++ b/plugins/map-key/src/api/removeSymbol.test.js @@ -0,0 +1,14 @@ +import { removeSymbol } from './removeSymbol.js' + +describe('removeSymbol', () => { + it('dispatches a REMOVE_KEY_SYMBOL action with the key definition', () => { + const dispatch = jest.fn() + + removeSymbol({ pluginState: { dispatch } }, { id: 'map-key' }) + + expect(dispatch).toHaveBeenCalledWith({ + type: 'REMOVE_KEY_SYMBOL', + payload: { id: 'map-key' } + }) + }) +}) diff --git a/plugins/map-key/src/components/Key/MapKey.jsx b/plugins/map-key/src/components/Key/MapKey.jsx index d6cfc9ea3..4b16fac9b 100755 --- a/plugins/map-key/src/components/Key/MapKey.jsx +++ b/plugins/map-key/src/components/Key/MapKey.jsx @@ -1,11 +1,12 @@ import React, { useState, useEffect } from 'react' import { getDatasetRegistry } from '../../registry/index.js' -import { mergeKeyGroupItems } from '../../utils/mergeKeyGroupItems.js' +import { mergeKeyGroupItems } from '../../reducers/mergeKeyGroupItems.js' import { Key } from './Key.jsx' export function MapKey ({ mapState: { mapStyle }, - pluginConfig: { noKeyItemText, groups }, + pluginConfig: { noKeyItemText }, + pluginState, services: { eventBus } }) { const [datasetRegistry, setDatasetRegistry] = useState(getDatasetRegistry()) @@ -18,7 +19,7 @@ export function MapKey ({ const getKeyItems = () => { const { items, hasGroups: _hasGroups } = datasetRegistry.keyItems() // Post Process the items - based on the map-key pluginConfig adding any groupConfigs - const groupItems = mergeKeyGroupItems(groups, items) + const groupItems = mergeKeyGroupItems(items) setKeyGroups(groupItems) setHasGroups(_hasGroups) } @@ -44,6 +45,13 @@ export function MapKey ({ } }, [datasetRegistry]) + useEffect(() => { + if (!datasetRegistry) { + return + } + getKeyItems() + }, [pluginState]) + return ( { if (!mapState.isMapReady) { return } + attachPluginStateRef(pluginStateRef) // Request a handle on the datasetsRegistry singleton eventBus.requestOnce('datasets:registry', setDatasetRegistry) + eventBus.emit('map-key:ready') }, [mapState.isMapReady]) + + useEffect(() => { + if (!pluginConfig.groups) { + return + } + const groups = Object.entries(pluginConfig.groups).map(([id, group]) => ({ ...group, id, label: group.groupLabel })) + + dispatch({ type: 'ADD_KEY_GROUPS', payload: groups }) + }, [pluginConfig.groups]) } diff --git a/plugins/map-key/src/initialise/MapKeyInit.test.jsx b/plugins/map-key/src/initialise/MapKeyInit.test.jsx index 4a359479f..db8c33983 100644 --- a/plugins/map-key/src/initialise/MapKeyInit.test.jsx +++ b/plugins/map-key/src/initialise/MapKeyInit.test.jsx @@ -6,34 +6,70 @@ jest.mock('../registry/getDatasetRegistry.js', () => ({ setDatasetRegistry: jest.fn() })) -const eventBus = { requestOnce: jest.fn() } -const services = { eventBus } +const eventBus = { requestOnce: jest.fn(), emit: jest.fn() } +const pluginState = { dispatch: jest.fn() } +const pluginConfig = { + groups: { + alpha: { groupLabel: 'Alpha' } + } +} beforeEach(() => { jest.clearAllMocks() }) describe('MapKeyInit', () => { + const renderInit = ({ isMapReady = false, config = {} } = {}) => render( + + ) + it('does not call requestOnce when map is not ready', () => { - render() + renderInit({ isMapReady: false }) expect(eventBus.requestOnce).not.toHaveBeenCalled() }) it('calls requestOnce for datasets:registry when map is ready', () => { - render() + renderInit({ isMapReady: true }) expect(eventBus.requestOnce).toHaveBeenCalledWith('datasets:registry', setDatasetRegistry) }) + it('dispatches configured groups when pluginConfig.groups is provided', () => { + renderInit({ isMapReady: true, config: pluginConfig }) + expect(pluginState.dispatch).toHaveBeenCalledWith({ + type: 'ADD_KEY_GROUPS', + payload: [{ id: 'alpha', groupLabel: 'Alpha', label: 'Alpha' }] + }) + }) + it('calls requestOnce when isMapReady changes to true', () => { - const { rerender } = render() + const { rerender } = renderInit({ isMapReady: false }) expect(eventBus.requestOnce).not.toHaveBeenCalled() - rerender() + rerender( + + ) expect(eventBus.requestOnce).toHaveBeenCalledTimes(1) }) it('does not call requestOnce again on re-render when isMapReady stays true', () => { - const { rerender } = render() - rerender() + const { rerender } = renderInit({ isMapReady: true }) + rerender( + + ) expect(eventBus.requestOnce).toHaveBeenCalledTimes(1) }) }) diff --git a/plugins/map-key/src/manifest.js b/plugins/map-key/src/manifest.js index 1cd51e2e3..b804e806b 100755 --- a/plugins/map-key/src/manifest.js +++ b/plugins/map-key/src/manifest.js @@ -1,8 +1,11 @@ import { MapKey } from './components/Key/MapKey.jsx' import { MapKeyInit } from './initialise/MapKeyInit.jsx' +import { initialState, actions } from './reducers/pluginState.js' +import { api } from './api/index.js' export const manifest = { InitComponent: MapKeyInit, + reducer: { initialState, actions }, panels: [ { id: 'mapKey', @@ -26,5 +29,6 @@ export const manifest = { icons: [{ id: 'key', svgContent: '' - }] + }], + api } diff --git a/plugins/map-key/src/reducers/mergeKeyGroupItems.js b/plugins/map-key/src/reducers/mergeKeyGroupItems.js new file mode 100644 index 000000000..0ba2a4104 --- /dev/null +++ b/plugins/map-key/src/reducers/mergeKeyGroupItems.js @@ -0,0 +1,68 @@ +let _pluginStateRef = {} +export const attachPluginStateRef = (pluginStateRef) => { _pluginStateRef = pluginStateRef } + +let previousRef = null +let addedKeyGroups = [] + +const getStateKeyGroups = () => { + if (!_pluginStateRef?.current) { + return [] + } + if (previousRef === _pluginStateRef.current) { + return addedKeyGroups + } + previousRef = _pluginStateRef.current + const { groups = [], keyDefinitions = [] } = _pluginStateRef.current + + addedKeyGroups = groups.map(group => { + if (group.type === 'flat') { + return { + ...group, + keyDefinition: keyDefinitions.find(item => item.id === group.id) + } + } + + return { + ...group, + keyDefinitions: keyDefinitions.filter(item => item.groupId === group.id) + } + }) + return addedKeyGroups +} + +// Merges the state key group items with items provided by another plugin, +// specifically the datasets plugin +export const mergeKeyGroupItems = (keyGroupItemsToMerge = []) => { + // Clone the current state key groups from the plugin state reference + // to avoid mutating the original array + const stateKeyGroups = [...getStateKeyGroups()] + if (stateKeyGroups.length === 0) { + // if there is nothing in the state key groups, just return the items that were passed in + return keyGroupItemsToMerge + } + + const mergedItems = keyGroupItemsToMerge.map((keyGroupItem) => { + const groupIndex = stateKeyGroups.findIndex((_group) => _group.id === keyGroupItem.id) + if (groupIndex === -1) { + return keyGroupItem + } + // Splice the matched group from the stateKeyGroups and merge it with the item + const group = stateKeyGroups.splice(groupIndex, 1)[0] + return { + ...keyGroupItem, + ...group, + keyDefinitions: [ + ...(group.keyDefinitions || []), + ...(keyGroupItem.keyDefinitions || []) + ] + } + }) + + // Append any remaining pluginConfigGroups that were not matched with datasetItems + // and filter out any groups that have no key definitions + const finalMergedItems = [...mergedItems, ...stateKeyGroups] + .filter(item => + ((item.type === 'flat' && item.keyDefinition) || + (item.type === 'group' && item.keyDefinitions?.length))) + return finalMergedItems +} diff --git a/plugins/map-key/src/reducers/mergeKeyGroupItems.test.js b/plugins/map-key/src/reducers/mergeKeyGroupItems.test.js new file mode 100644 index 000000000..362c80283 --- /dev/null +++ b/plugins/map-key/src/reducers/mergeKeyGroupItems.test.js @@ -0,0 +1,91 @@ +import { attachPluginStateRef, mergeKeyGroupItems } from './mergeKeyGroupItems.js' + +describe('mergeKeyGroupItems', () => { + afterEach(() => { + attachPluginStateRef({ current: null }) + }) + + it('returns the incoming items unchanged when no plugin state is attached', () => { + attachPluginStateRef({ current: null }) + const items = [{ id: 'dataset-1', keyDefinitions: [{ id: 'k1' }] }] + + expect(mergeKeyGroupItems(items)).toBe(items) + }) + + it('returns dataset items unchanged when plugin state groups are empty', () => { + attachPluginStateRef({ current: { groups: [], keyDefinitions: [] } }) + const items = [{ id: 'dataset-1', keyDefinitions: [{ id: 'k1' }] }] + + expect(mergeKeyGroupItems(items)).toEqual(items) + }) + + it('uses default empty state values and empty arguments when no groups or items are supplied', () => { + attachPluginStateRef({ current: {} }) + + expect(mergeKeyGroupItems()).toEqual([]) + }) + + it('merges a matched group even when the state group has no keyDefinitions array', () => { + attachPluginStateRef({ + current: { + groups: [{ id: 'dataset-1', type: 'group', groupLabel: 'Dataset 1' }], + keyDefinitions: [] + } + }) + + const items = [{ id: 'dataset-1', keyDefinitions: [{ id: 'd1' }] }] + + expect(mergeKeyGroupItems(items)).toEqual([ + { id: 'dataset-1', type: 'group', groupLabel: 'Dataset 1', keyDefinitions: [{ id: 'd1' }] } + ]) + }) + + it('reuses the cached plugin-state groups when the ref object is unchanged', () => { + const ref = { current: { groups: [{ id: 'group-1', type: 'group', groupLabel: 'Group 1' }], keyDefinitions: [{ id: 'k1', groupId: 'group-1' }] } } + attachPluginStateRef(ref) + + const first = mergeKeyGroupItems([{ id: 'dataset-1', keyDefinitions: [{ id: 'd1' }] }]) + const second = mergeKeyGroupItems([{ id: 'dataset-1', keyDefinitions: [{ id: 'd2' }] }]) + + expect(first).toEqual([{ id: 'group-1', type: 'group', groupLabel: 'Group 1', keyDefinitions: [{ id: 'k1', groupId: 'group-1' }] }]) + expect(second).toEqual(first) + }) + + it('merges a matching flat group into the dataset item when the key definition matches the same id', () => { + attachPluginStateRef({ + current: { + groups: [{ id: 'dataset-1', type: 'flat' }], + keyDefinitions: [{ id: 'dataset-1' }] + } + }) + + const items = [{ id: 'dataset-1', keyDefinitions: [{ id: 'dataset-1' }] }] + + expect(mergeKeyGroupItems(items)).toEqual([ + { + id: 'dataset-1', + type: 'flat', + keyDefinition: { id: 'dataset-1' }, + keyDefinitions: [{ id: 'dataset-1' }] + } + ]) + }) + + it('keeps only unmatched state groups that still have key definitions', () => { + attachPluginStateRef({ + current: { + groups: [ + { id: 'group-1', type: 'group', groupLabel: 'Group 1' }, + { id: 'group-2', type: 'group', groupLabel: 'Group 2' } + ], + keyDefinitions: [{ id: 'k1', groupId: 'group-1' }] + } + }) + + const items = [{ id: 'dataset-1', keyDefinitions: [{ id: 'd1' }] }] + + expect(mergeKeyGroupItems(items)).toEqual([ + { id: 'group-1', type: 'group', groupLabel: 'Group 1', keyDefinitions: [{ id: 'k1', groupId: 'group-1' }] } + ]) + }) +}) diff --git a/plugins/map-key/src/reducers/pluginState.js b/plugins/map-key/src/reducers/pluginState.js new file mode 100644 index 000000000..316819094 --- /dev/null +++ b/plugins/map-key/src/reducers/pluginState.js @@ -0,0 +1,68 @@ +const initialState = { + keyDefinitions: [], + groups: [] +} + +const labelToId = (label) => label ? label.toLowerCase().replace(/\s+/g, '-') : null +const createNewGroup = (id, groupId, groupLabel) => { + return groupId + ? { id: groupId, type: 'group', groupLabel } + : { id, type: 'flat' } +} + +const findGroup = (state, id, groupId) => { + return state.groups.find(group => { + return (group.type === 'group' && group.id === groupId) || + (group.type === 'flat' && group.id === id) + }) +} + +const addSymbol = (state, keyDefinition) => { + const { id, groupLabel } = keyDefinition + const groupId = keyDefinition.groupId || labelToId(groupLabel) + // Check for an existingGroup + const existingGroup = findGroup(state, id, groupId) + // If no existing group is found, create a new one + const newGroup = existingGroup ? null : createNewGroup(id, groupId, groupLabel) + // Add the new group to the state if it was created + const groups = newGroup ? [...state.groups, newGroup] : state.groups + // Add the key definition to the state + const keyDefinitions = [...state.keyDefinitions, { + ...keyDefinition, + type: 'manual', + ...(groupId ? { groupId } : {}) + }] + + // Return the updated state with the new key definitions and groups + return { + ...state, + keyDefinitions, + groups + } +} + +const removeSymbol = (state, keyDefinition) => { + const { id } = keyDefinition + return { + ...state, + keyDefinitions: state.keyDefinitions.filter(key => key.id !== id) + } +} + +const addKeyGroups = (state, groups) => { + return { + ...state, + groups: [...state.groups, ...groups] + } +} + +const actions = { + ADD_KEY_SYMBOL: addSymbol, + REMOVE_KEY_SYMBOL: removeSymbol, + ADD_KEY_GROUPS: addKeyGroups +} + +export { + initialState, + actions +} diff --git a/plugins/map-key/src/reducers/pluginState.test.js b/plugins/map-key/src/reducers/pluginState.test.js new file mode 100644 index 000000000..9c031c806 --- /dev/null +++ b/plugins/map-key/src/reducers/pluginState.test.js @@ -0,0 +1,82 @@ +import { actions, initialState } from './pluginState.js' + +describe('pluginState reducer helpers', () => { + it('returns the initial state', () => { + expect(initialState).toEqual({ keyDefinitions: [], groups: [] }) + }) + + it('adds a flat key item and creates a flat group when needed', () => { + const nextState = actions.ADD_KEY_SYMBOL(initialState, { id: 'flat-key', label: 'Flat key' }) + + expect(nextState).toEqual({ + keyDefinitions: [{ id: 'flat-key', label: 'Flat key', type: 'manual' }], + groups: [{ id: 'flat-key', type: 'flat' }] + }) + }) + + it('adds a grouped key item when the label can be converted to a group id', () => { + const nextState = actions.ADD_KEY_SYMBOL(initialState, { id: 'group-key', groupLabel: 'Group Label', label: 'Group key' }) + + expect(nextState).toEqual({ + keyDefinitions: [{ + id: 'group-key', + groupId: 'group-label', + groupLabel: 'Group Label', + label: 'Group key', + type: 'manual' + }], + groups: [{ id: 'group-label', type: 'group', groupLabel: 'Group Label' }] + }) + }) + + it('reuses an existing group when the key item matches a flat group already in state', () => { + const state = { + keyDefinitions: [{ id: 'existing-item' }], + groups: [{ id: 'existing-item', type: 'flat' }] + } + + expect(actions.ADD_KEY_SYMBOL(state, { id: 'existing-item', label: 'Existing item' })).toEqual({ + keyDefinitions: [ + { id: 'existing-item' }, + { id: 'existing-item', label: 'Existing item', type: 'manual' } + ], + groups: [{ id: 'existing-item', type: 'flat' }] + }) + }) + + it('reuses an existing grouped definition when the same group id is already present', () => { + const state = { + keyDefinitions: [{ id: 'existing-group-item', groupId: 'group-1' }], + groups: [{ id: 'group-1', type: 'group', groupLabel: 'Group 1' }] + } + + expect(actions.ADD_KEY_SYMBOL(state, { id: 'new-item', groupId: 'group-1', groupLabel: 'Group 1', label: 'New item' })).toEqual({ + keyDefinitions: [ + { id: 'existing-group-item', groupId: 'group-1' }, + { id: 'new-item', groupId: 'group-1', groupLabel: 'Group 1', label: 'New item', type: 'manual' } + ], + groups: [{ id: 'group-1', type: 'group', groupLabel: 'Group 1' }] + }) + }) + + it('removes a key definition by id', () => { + const state = { + keyDefinitions: [{ id: 'a' }, { id: 'b' }], + groups: [{ id: 'a', type: 'flat' }] + } + + expect(actions.REMOVE_KEY_SYMBOL(state, { id: 'a' })).toEqual({ + keyDefinitions: [{ id: 'b' }], + groups: [{ id: 'a', type: 'flat' }] + }) + }) + + it('adds configured groups to state without mutating the source array', () => { + const groups = [{ id: 'g1', type: 'group', groupLabel: 'G1' }] + + expect(actions.ADD_KEY_GROUPS(initialState, groups)).toEqual({ + keyDefinitions: [], + groups: [{ id: 'g1', type: 'group', groupLabel: 'G1' }] + }) + }) +}) diff --git a/plugins/map-key/src/utils/mergeKeyGroupItems.js b/plugins/map-key/src/utils/mergeKeyGroupItems.js deleted file mode 100644 index b547a2fcf..000000000 --- a/plugins/map-key/src/utils/mergeKeyGroupItems.js +++ /dev/null @@ -1,12 +0,0 @@ -export const mergeKeyGroupItems = (groups, items) => { - if (!groups) { - return items - } - return items.map((item) => { - const { id } = item - if (groups[id]) { - return { ...item, ...groups[id] } - } - return item - }) -} diff --git a/plugins/map-key/src/utils/mergeKeyGroupItems.test.js b/plugins/map-key/src/utils/mergeKeyGroupItems.test.js deleted file mode 100644 index 0fcdf6d2a..000000000 --- a/plugins/map-key/src/utils/mergeKeyGroupItems.test.js +++ /dev/null @@ -1,53 +0,0 @@ -import { mergeKeyGroupItems } from './mergeKeyGroupItems' - -describe('mergeKeyGroupItems', () => { - it('returns items unchanged when groups is not provided', () => { - const items = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }] - - expect(mergeKeyGroupItems(undefined, items)).toBe(items) - }) - - it('returns items unchanged when groups is null', () => { - const items = [{ id: 'a', label: 'A' }] - - expect(mergeKeyGroupItems(null, items)).toBe(items) - }) - - it('merges matching group data into an item', () => { - const items = [{ id: 'a', label: 'A' }] - const groups = { a: { group: 'Group 1' } } - - expect(mergeKeyGroupItems(groups, items)).toEqual([ - { id: 'a', label: 'A', group: 'Group 1' } - ]) - }) - - it('overrides item properties with matching group properties', () => { - const items = [{ id: 'a', label: 'A' }] - const groups = { a: { label: 'Overridden' } } - - expect(mergeKeyGroupItems(groups, items)).toEqual([ - { id: 'a', label: 'Overridden' } - ]) - }) - - it('leaves items without a matching group unchanged', () => { - const items = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }] - const groups = { a: { group: 'Group 1' } } - - expect(mergeKeyGroupItems(groups, items)).toEqual([ - { id: 'a', label: 'A', group: 'Group 1' }, - { id: 'b', label: 'B' } - ]) - }) - - it('returns an empty array when items is empty', () => { - expect(mergeKeyGroupItems({ a: { group: 'Group 1' } }, [])).toEqual([]) - }) - - it('handles an empty groups object as no matches', () => { - const items = [{ id: 'a', label: 'A' }] - - expect(mergeKeyGroupItems({}, items)).toEqual(items) - }) -})