From e4f5851f76139ed06bc3049d9aa9433f7b3749e4 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Tue, 9 Apr 2024 12:08:09 +0200 Subject: [PATCH] Add context for array operation Currently it is very hard to figure out in the middleware what array operation happend to lead to the current array state. There are cases were this information is valuable. To support this all update actions can now have a 'context'. Here we added a specific 'UpdateArrayContext'. Contributed on behalf of STMicroelectronics --- packages/core/src/actions/actions.ts | 59 +++++++++++++++++- packages/core/src/util/renderer.ts | 70 ++++++++++++++-------- packages/core/test/actions/actions.test.ts | 70 ++++++++++++++++++++++ 3 files changed, 174 insertions(+), 25 deletions(-) diff --git a/packages/core/src/actions/actions.ts b/packages/core/src/actions/actions.ts index c167dba198..3f899ab9e1 100644 --- a/packages/core/src/actions/actions.ts +++ b/packages/core/src/actions/actions.ts @@ -56,6 +56,60 @@ export const UPDATE_I18N = 'jsonforms/UPDATE_I18N' as const; export const ADD_DEFAULT_DATA = 'jsonforms/ADD_DEFAULT_DATA' as const; export const REMOVE_DEFAULT_DATA = 'jsonforms/REMOVE_DEFAULT_DATA' as const; +export type UpdateArrayContext = + | { type: 'ADD'; values: any[] } + | { type: 'REMOVE'; indices: number[] } + | { type: 'MOVE'; moves: { from: number; to: number }[] }; + +export const isUpdateArrayContext = ( + context: object +): context is UpdateArrayContext => { + if (!('type' in context)) { + return false; + } + if (typeof context.type !== 'string') { + return false; + } + switch (context.type) { + case 'ADD': { + if ( + 'values' in context && + Array.isArray(context.values) && + context.values.length > 0 + ) { + const firstValueType = typeof context.values[0]; + return context.values.every((v) => typeof v === firstValueType); + } + return false; + } + case 'REMOVE': { + return ( + 'indices' in context && + Array.isArray(context.indices) && + context.indices.length > 0 && + context.indices.every((i) => typeof i === 'number') + ); + } + case 'MOVE': { + return ( + 'moves' in context && + Array.isArray(context.moves) && + context.moves.length > 0 && + context.moves.every( + (m) => + typeof m === 'object' && + 'from' in m && + 'to' in m && + typeof m.from === 'number' && + typeof m.to === 'number' + ) + ); + } + default: + return false; + } +}; + export type CoreActions = | InitAction | UpdateCoreAction @@ -70,6 +124,7 @@ export interface UpdateAction { type: 'jsonforms/UPDATE'; path: string; updater(existingData?: any): any; + context?: object; } export interface UpdateErrorsAction { @@ -165,11 +220,13 @@ export const setAjv = (ajv: AJV) => ({ export const update = ( path: string, - updater: (existingData: any) => any + updater: (existingData: any) => any, + context?: object ): UpdateAction => ({ type: UPDATE_DATA, path, updater, + context, }); export const updateErrors = (errors: ErrorObject[]): UpdateErrorsAction => ({ diff --git a/packages/core/src/util/renderer.ts b/packages/core/src/util/renderer.ts index 1346efdef4..004e75a8ac 100644 --- a/packages/core/src/util/renderer.ts +++ b/packages/core/src/util/renderer.ts @@ -59,7 +59,7 @@ import { moveDown, moveUp } from './array'; import type { AnyAction, Dispatch } from './type'; import { Resolve, convertDateToString, hasType } from './util'; import { composePaths, composeWithUi } from './path'; -import { CoreActions, update } from '../actions'; +import { CoreActions, update, UpdateArrayContext } from '../actions'; import type { ErrorObject } from 'ajv'; import type { JsonFormsState } from '../store'; import { @@ -823,41 +823,63 @@ export const mapDispatchToArrayControlProps = ( ): DispatchPropsOfArrayControl => ({ addItem: (path: string, value: any) => () => { dispatch( - update(path, (array) => { - if (array === undefined || array === null) { - return [value]; - } - - array.push(value); - return array; - }) + update( + path, + (array) => { + if (array === undefined || array === null) { + return [value]; + } + + array.push(value); + return array; + }, + { type: 'ADD', values: [value] } as UpdateArrayContext + ) ); }, removeItems: (path: string, toDelete: number[]) => () => { dispatch( - update(path, (array) => { - toDelete - .sort((a, b) => a - b) - .reverse() - .forEach((s) => array.splice(s, 1)); - return array; - }) + update( + path, + (array) => { + toDelete + .sort((a, b) => a - b) + .reverse() + .forEach((s) => array.splice(s, 1)); + return array; + }, + { type: 'REMOVE', indices: toDelete } as UpdateArrayContext + ) ); }, moveUp: (path, toMove: number) => () => { dispatch( - update(path, (array) => { - moveUp(array, toMove); - return array; - }) + update( + path, + (array) => { + moveUp(array, toMove); + return array; + }, + { + type: 'MOVE', + moves: [{ from: toMove, to: toMove - 1 }], + } as UpdateArrayContext + ) ); }, moveDown: (path, toMove: number) => () => { dispatch( - update(path, (array) => { - moveDown(array, toMove); - return array; - }) + update( + path, + (array) => { + moveDown(array, toMove); + return array; + }, + { + type: 'MOVE', + moves: [{ from: toMove, to: toMove + 1 }], + } as UpdateArrayContext + ) ); }, }); diff --git a/packages/core/test/actions/actions.test.ts b/packages/core/test/actions/actions.test.ts index 35ffe3debd..1b4758416a 100644 --- a/packages/core/test/actions/actions.test.ts +++ b/packages/core/test/actions/actions.test.ts @@ -91,3 +91,73 @@ test('Init Action generates ui schema when not valid', (t) => { ], } as UISchemaElement); }); + +test('isUpdateArrayContext correctly identifies ', (t) => { + t.deepEqual(Actions.isUpdateArrayContext({}), false); + t.deepEqual(Actions.isUpdateArrayContext({ type: 'ADD' }), false); + t.deepEqual(Actions.isUpdateArrayContext({ type: 'ADD', values: [] }), false); + t.deepEqual( + Actions.isUpdateArrayContext({ type: 'ADD', values: [0, ''] }), + false + ); + t.deepEqual( + Actions.isUpdateArrayContext({ type: 'ADD', values: [0, 2] }), + true + ); + + t.deepEqual(Actions.isUpdateArrayContext({ type: 'REMOVE' }), false); + t.deepEqual( + Actions.isUpdateArrayContext({ type: 'REMOVE', indices: [] }), + false + ); + t.deepEqual( + Actions.isUpdateArrayContext({ type: 'REMOVE', indices: [0, ''] }), + false + ); + t.deepEqual( + Actions.isUpdateArrayContext({ type: 'REMOVE', indices: [0, 2] }), + true + ); + + t.deepEqual(Actions.isUpdateArrayContext({ type: 'MOVE' }), false); + t.deepEqual(Actions.isUpdateArrayContext({ type: 'MOVE', moves: [] }), false); + t.deepEqual( + Actions.isUpdateArrayContext({ type: 'MOVE', moves: [0] }), + false + ); + t.deepEqual( + Actions.isUpdateArrayContext({ + type: 'MOVE', + moves: [{ from: 0, to: 1 }, { from: 2 }], + }), + false + ); + t.deepEqual( + Actions.isUpdateArrayContext({ + type: 'MOVE', + moves: [{ from: 0, to: 1 }, { to: 0 }], + }), + false + ); + t.deepEqual( + Actions.isUpdateArrayContext({ + type: 'MOVE', + moves: [{ from: 0, to: '' }], + }), + false + ); + t.deepEqual( + Actions.isUpdateArrayContext({ + type: 'MOVE', + moves: [ + { from: 0, to: 1 }, + { from: 0, to: '' }, + ], + }), + false + ); + t.deepEqual( + Actions.isUpdateArrayContext({ type: 'MOVE', moves: [{ from: 0, to: 1 }] }), + true + ); +});