diff --git a/package.json b/package.json index d66e8ffbb..5b1f894ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@inseefr/lunatic", - "version": "3.4.11", + "version": "3.4.12-rc.0", "description": "Library of questionnaire components", "repository": { "type": "git", diff --git a/src/components/Loop/Loop.tsx b/src/components/Loop/Loop.tsx index db5682b7a..2c7ab5cbe 100644 --- a/src/components/Loop/Loop.tsx +++ b/src/components/Loop/Loop.tsx @@ -51,31 +51,61 @@ export function Loop({ } }, [nbRows, handleChanges, value]); + const removeRowWithIndex = useCallback( + (indexToRemove: number) => { + if (nbRows <= min) { + return; + } + const newResponses = Object.entries(value).map(([k, v]) => { + return { + name: k, + value: v?.filter((_, i) => i !== indexToRemove), + removedIndex: indexToRemove, + }; + }); + handleChanges(newResponses); + setNbRows((n) => n - 1); + }, + [nbRows, min, value, handleChanges] + ); + if (nbRows <= 0) { return null; } + const canControlRows = min !== max && Number.isFinite(max); + return ( {times(nbRows, (n) => ( - ({ - ...props, - ...c, - iteration: n, - id: `${c.id}-${n}`, - errors, - })} - /> + <> + ({ + ...props, + ...c, + iteration: n, + id: `${c.id}-${n}`, + errors, + })} + /> + {canControlRows && ( + - + )} ); diff --git a/src/components/RosterForLoop/RosterForLoop.tsx b/src/components/RosterForLoop/RosterForLoop.tsx index 7f05f4c93..7a86dd36e 100644 --- a/src/components/RosterForLoop/RosterForLoop.tsx +++ b/src/components/RosterForLoop/RosterForLoop.tsx @@ -2,6 +2,7 @@ import { Fragment, useCallback, useState } from 'react'; import type { LunaticComponentProps } from '../type'; import { Table, Tbody, Td, Tr, TableHeader } from '../shared/Table'; import { times } from '../../utils/array'; +import D from '../../i18n'; import { LunaticComponents } from '../LunaticComponents'; import { blockedInLoopComponents } from '../Loop/constant'; import { @@ -9,6 +10,7 @@ import { getComponentErrors, } from '../shared/ComponentErrors/ComponentErrors'; import { CustomLoop } from '../Loop/Loop'; +import { Button } from '../shared/Button/Button'; const DEFAULT_MIN_ROWS = 1; const DEFAULT_MAX_ROWS = 12; @@ -44,6 +46,8 @@ export const RosterForLoop = ( } }, [max, nbRows]); + const cantRemove = nbRows === min; + const removeRow = useCallback(() => { if (nbRows <= min) { return; @@ -60,6 +64,28 @@ export const RosterForLoop = ( handleChanges(newResponses); }, [nbRows, min, valueMap, handleChanges]); + const removeRowWithIndex = useCallback( + (indexToRemove: number) => { + if (nbRows <= min) { + return; + } + // trying to delete with indexToRemove out of array index + if (indexToRemove >= nbRows || indexToRemove < 0) { + return; + } + const newResponses = Object.entries(valueMap).map(([k, v]) => { + return { + name: k, + value: v?.filter((_, i) => i !== indexToRemove), + removedIndex: indexToRemove, + }; + }); + handleChanges(newResponses); + setNbRows((n) => n - 1); + }, + [nbRows, min, valueMap, handleChanges] + ); + if (nbRows === 0) { return null; } @@ -71,11 +97,13 @@ export const RosterForLoop = ( {...props} errors={getComponentErrors(errors, props.id)} addRow={nbRows === max ? undefined : addRow} - removeRow={nbRows === min ? undefined : removeRow} + removeRow={cantRemove ? undefined : removeRow} canControlRows={!!(min && max && min !== max)} > - {header && } + {header && ( + + )} {times(nbRows, (n) => { const components = getComponents(n); @@ -104,6 +132,14 @@ export const RosterForLoop = ( })} wrapper={(props) => {hasLineErrors && ( diff --git a/src/components/RosterForLoop/__snapshots__/RosterForLoop.spec.tsx.snap b/src/components/RosterForLoop/__snapshots__/RosterForLoop.spec.tsx.snap index 67d48fcc5..4215a6647 100644 --- a/src/components/RosterForLoop/__snapshots__/RosterForLoop.spec.tsx.snap +++ b/src/components/RosterForLoop/__snapshots__/RosterForLoop.spec.tsx.snap @@ -45,6 +45,16 @@ exports[`RosterForLoop > renders the right number of columns 1`] = ` + renders the right number of columns 1`] = ` +
} /> + +
+ +
+ +
- - +
+ + +
`; diff --git a/src/i18n/dictionary.ts b/src/i18n/dictionary.ts index beee52b47..612fca754 100644 --- a/src/i18n/dictionary.ts +++ b/src/i18n/dictionary.ts @@ -1,6 +1,11 @@ const dictionary = { DEFAULT_BUTTON_ADD: { fr: 'Ajouter une ligne', en: 'Add row' }, DEFAULT_BUTTON_REMOVE: { fr: 'Supprimer une ligne', en: 'Remove row' }, + DEFAULT_BUTTON_REMOVE_THIS_ROW: { + fr: 'Supprimer cette ligne', + en: 'Remove this row', + }, + ACTION_HEADER: { fr: 'Action', en: 'Action' }, MODAL_IGNORE: { fr: 'Poursuivre', en: 'Ignore' }, MODAL_CORRECT: { fr: 'Corriger ma réponse', en: 'Correct' }, DK: { fr: 'Ne sais pas', en: "Don't know" }, diff --git a/src/stories/pairwise/data.json b/src/stories/pairwise/data.json index 532a502c9..b54810d0c 100644 --- a/src/stories/pairwise/data.json +++ b/src/stories/pairwise/data.json @@ -1,6 +1,6 @@ { "COLLECTED": { - "PRENOM": { "COLLECTED": ["Dad", "Mom", "Unknow"] }, + "PRENOM": { "COLLECTED": ["Dad", "Mom", "Daughter"] }, "AGE": { "COLLECTED": [30, 29, 5] }, "LINKS": { "COLLECTED": [[null]] diff --git a/src/stories/pairwise/source.json b/src/stories/pairwise/source.json index 7741435d6..fa245fda0 100644 --- a/src/stories/pairwise/source.json +++ b/src/stories/pairwise/source.json @@ -341,7 +341,9 @@ "resizing": { "PRENOM": { "sizeForLinksVariables": ["count(PRENOM)", "count(PRENOM)"], - "linksVariables": ["LINKS"] + "linksVariables": ["LINKS"], + "size": "count(PRENOM)", + "variables": ["AGE"] } } } diff --git a/src/type.source.ts b/src/type.source.ts index 0fff66c92..bd2117e70 100644 --- a/src/type.source.ts +++ b/src/type.source.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ /** * This file was automatically generated by json-schema-to-typescript. * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, diff --git a/src/use-lunatic/__snapshots__/use-lunatic.test.ts.snap b/src/use-lunatic/__snapshots__/use-lunatic.test.ts.snap index f47f7134f..a78032a56 100644 --- a/src/use-lunatic/__snapshots__/use-lunatic.test.ts.snap +++ b/src/use-lunatic/__snapshots__/use-lunatic.test.ts.snap @@ -1855,329 +1855,6 @@ exports[`use-lunatic() > overview > with loop > should handle initialPage 1`] = ] `; -exports[`use-lunatic() > overview > with loop > should handle lastReachedPage 1`] = ` -[ - { - "children": [ - { - "children": [], - "current": false, - "description": undefined, - "id": "lujqeci5", - "label": , - "page": "2", - "reached": true, - "type": "Subsequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "lujqbyzl", - "label": , - "page": "3", - "reached": true, - "type": "Subsequence", - }, - ], - "current": true, - "description": undefined, - "id": "lujqfpva", - "label": , - "page": "1", - "reached": true, - "type": "Sequence", - }, - { - "children": [ - { - "children": [], - "current": false, - "description": undefined, - "id": "lulbmyhr", - "label": , - "page": "8", - "reached": true, - "type": "Subsequence", - }, - ], - "current": false, - "description": undefined, - "id": "lujqrqmp", - "label": , - "page": "5", - "reached": true, - "type": "Sequence", - }, - { - "children": [ - { - "children": [], - "current": false, - "description": undefined, - "id": "lujykwaz", - "label": , - "page": "10.1#1", - "reached": true, - "type": "Subsequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "lujykwaz", - "label": , - "page": "10.1#2", - "reached": true, - "type": "Subsequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "lujykwaz", - "label": , - "page": "10.1#3", - "reached": true, - "type": "Subsequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "lujyik5q", - "label": , - "page": "11.1#2", - "reached": true, - "type": "Subsequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "luk0swcz", - "label": , - "page": "11.2#2", - "reached": true, - "type": "Subsequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "lujyik5q", - "label": , - "page": "11.1#3", - "reached": false, - "type": "Subsequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "luk0swcz", - "label": , - "page": "11.2#3", - "reached": false, - "type": "Subsequence", - }, - ], - "current": false, - "description": undefined, - "id": "lujyi4pe", - "label": , - "page": "9", - "reached": true, - "type": "Sequence", - }, - { - "children": [ - { - "children": [], - "current": false, - "description": undefined, - "id": "lumfc98o", - "label": , - "page": "12.2#1", - "reached": false, - "type": "Subsequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "lumfe3bj", - "label": , - "page": "12.3#1", - "reached": false, - "type": "Subsequence", - }, - ], - "current": false, - "description": undefined, - "id": "luk1ojt5", - "label": , - "page": "12.1#1", - "reached": false, - "type": "Sequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "lulbelgr", - "label": , - "page": "12.4#1", - "reached": false, - "type": "Sequence", - }, - { - "children": [ - { - "children": [], - "current": false, - "description": undefined, - "id": "lumfc98o", - "label": , - "page": "12.2#2", - "reached": false, - "type": "Subsequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "lumfe3bj", - "label": , - "page": "12.3#2", - "reached": false, - "type": "Subsequence", - }, - ], - "current": false, - "description": undefined, - "id": "luk1ojt5", - "label": , - "page": "12.1#2", - "reached": false, - "type": "Sequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "lulbelgr", - "label": , - "page": "12.4#2", - "reached": false, - "type": "Sequence", - }, - { - "children": [ - { - "children": [], - "current": false, - "description": undefined, - "id": "lumfc98o", - "label": , - "page": "12.2#3", - "reached": false, - "type": "Subsequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "lumfe3bj", - "label": , - "page": "12.3#3", - "reached": false, - "type": "Subsequence", - }, - ], - "current": false, - "description": undefined, - "id": "luk1ojt5", - "label": , - "page": "12.1#3", - "reached": false, - "type": "Sequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "lulbelgr", - "label": , - "page": "12.4#3", - "reached": false, - "type": "Sequence", - }, - { - "children": [], - "current": false, - "description": undefined, - "id": "COMMENT-SEQ", - "label": , - "page": "13", - "reached": false, - "type": "Sequence", - }, -] -`; - exports[`use-lunatic() > overview > with loop > should work with loop 1`] = ` [ { diff --git a/src/use-lunatic/commons/variables/behaviours/resizing-behaviour.ts b/src/use-lunatic/commons/variables/behaviours/resizing-behaviour.ts index 941de8324..a718c36ad 100644 --- a/src/use-lunatic/commons/variables/behaviours/resizing-behaviour.ts +++ b/src/use-lunatic/commons/variables/behaviours/resizing-behaviour.ts @@ -1,9 +1,8 @@ import type { LunaticVariablesStore } from '../lunatic-variables-store'; import type { LunaticSource } from '../../../type'; import { forceInt } from '../../../../utils/number'; -import { resizeArrayVariable } from '../../../reducer/commons'; import { getExpressionAsString } from '../../../../utils/vtl'; -import { resizeArray } from '../../../../utils/array'; +import { resizeArray, resizeDownArrayWithIndex } from '../../../../utils/array'; /** * Resizing behaviour for the store @@ -42,8 +41,19 @@ export function resizingBehaviour( const newSize = forceInt(store.run(resizingInfo.size)); for (const variableName of resizingInfo.variables) { const value = store.get(variableName); - if (!Array.isArray(value) || value.length !== newSize) { - store.set(variableName, resizeArrayVariable(value, newSize, null), { + if (Array.isArray(value) && e.detail.removedIndex !== undefined) { + store.set( + variableName, + resizeDownArrayWithIndex(value, e.detail.removedIndex), + { + cause: 'resizing', + } + ); + } else if ( + e.detail.removedIndex === undefined && + (!Array.isArray(value) || value.length !== newSize) + ) { + store.set(variableName, resizeArray(value, newSize, null), { cause: 'resizing', }); } @@ -61,6 +71,7 @@ function resizePairwise( }, args: { iteration?: number[]; + removedIndex?: number; } ) { // Handle expression being sent as an array or an object (ensure backward compatibility) @@ -78,12 +89,25 @@ function resizePairwise( }); resizingInfo.linksVariables.forEach((variable) => { const value = store.get(variable, args.iteration); - const resizedValue = resizeArray( - // The value is not an array, force an array - Array.isArray(value) ? value.map((i) => resizeArray(i, ySize, null)) : [], - xSize, - new Array(ySize).fill(null) - ); + let resizedValue; + if (args.removedIndex !== undefined) { + const removedIndex = args.removedIndex; + resizedValue = resizeDownArrayWithIndex( + Array.isArray(value) + ? value.map((i) => resizeDownArrayWithIndex(i, removedIndex)) + : [], + removedIndex + ); + } else { + resizedValue = resizeArray( + // The value is not an array, force an array + Array.isArray(value) + ? value.map((i) => resizeArray(i, ySize, null)) + : [], + xSize, + new Array(ySize).fill(null) + ); + } store.set(variable, resizedValue); }); } diff --git a/src/use-lunatic/commons/variables/lunatic-variables-store.spec.ts b/src/use-lunatic/commons/variables/lunatic-variables-store.spec.ts index 0cda1fc68..baec44525 100644 --- a/src/use-lunatic/commons/variables/lunatic-variables-store.spec.ts +++ b/src/use-lunatic/commons/variables/lunatic-variables-store.spec.ts @@ -281,6 +281,36 @@ describe('lunatic-variables-store', () => { cause: 'resizing', }); }); + + it('should resize variables with Index', () => { + variables.set('PRENOM', ['John', 'Jane', 'Marc']); + variables.set('AGE', [20, 30, 40]); + const spy = vi.fn(); + variables.on('change', (e) => spy(e.detail)); + resizingBehaviour(variables, { + PRENOM: { + size: 'count(PRENOM)', + variables: ['AGE'], + }, + }); + variables.set('PRENOM', ['John', 'Marc'], { removedIndex: 1 }); + expect((variables.get('PRENOM') as string[]).length).toEqual(2); + expect((variables.get('AGE') as string[]).length).toEqual(2); + expect(spy).toHaveBeenLastCalledWith({ + name: 'AGE', + value: [20, 40], + cause: 'resizing', + }); + variables.set('PRENOM', ['Marc'], { removedIndex: 0 }); + expect((variables.get('PRENOM') as string[]).length).toEqual(1); + expect((variables.get('AGE') as string[]).length).toEqual(1); + expect(spy).toHaveBeenLastCalledWith({ + name: 'AGE', + value: [40], + cause: 'resizing', + }); + }); + it('should resize pairwise with the array syntax', () => { variables.set('PRENOM', []); variables.set('LINKS', [[]]); @@ -316,7 +346,29 @@ describe('lunatic-variables-store', () => { [null, null, null], ]); }); - it('should handle both pairwise and normal resize', () => { + it('should resize pairwise with the object syntax with index', () => { + variables.set('PRENOM', ['John', 'Jane', 'Marc']); + variables.set('LINKS', [ + [null, 2, 4], + [1, null, 2], + [3, 2, null], + ]); + resizingBehaviour(variables, { + PRENOM: { + sizeForLinksVariables: { + xAxisSize: 'count(PRENOM)', + yAxisSize: 'count(PRENOM)', + }, + linksVariables: ['LINKS'], + }, + }); + variables.set('PRENOM', ['John', 'Marc'], { removedIndex: 1 }); + expect(variables.get('LINKS') as string[][]).toEqual([ + [null, 4], + [3, null], + ]); + }); + it('should handle both: pairwise and normal resize', () => { variables.set('PRENOM', []); variables.set('NOM', []); variables.set('LINKS', [[]]); @@ -336,6 +388,50 @@ describe('lunatic-variables-store', () => { ]); expect(variables.get('NOM') as string[]).toEqual([null, null, null]); }); + it('should handle both: pairwise resize with index 0', () => { + variables.set('PRENOM', ['John', 'Jane', 'Marc']); + variables.set('LINKS', [ + [null, 2, 4], + [1, null, 2], + [3, 2, null], + ]); + resizingBehaviour(variables, { + PRENOM: { + sizeForLinksVariables: ['count(PRENOM)', 'count(PRENOM)'], + linksVariables: ['LINKS'], + size: 'count(PRENOM)', + }, + }); + variables.set('PRENOM', ['John', 'Marc'], { removedIndex: 0 }); + expect(variables.get('LINKS') as string[][]).toEqual([ + [null, 2], + [2, null], + ]); + }); + + it('should handle both: pairwise and normal resize with index', () => { + variables.set('PRENOM', ['John', 'Jane', 'Marc']); + variables.set('AGE', [40, 30, 20]); + variables.set('LINKS', [ + [null, 2, 4], + [1, null, 2], + [3, 2, null], + ]); + resizingBehaviour(variables, { + PRENOM: { + sizeForLinksVariables: ['count(PRENOM)', 'count(PRENOM)'], + linksVariables: ['LINKS'], + size: 'count(PRENOM)', + variables: ['AGE'], + }, + }); + variables.set('PRENOM', ['John', 'Marc'], { removedIndex: 1 }); + expect(variables.get('LINKS') as string[][]).toEqual([ + [null, 4], + [3, null], + ]); + expect(variables.get('AGE') as string[]).toEqual([40, 20]); + }); }); describe('cleaning', () => { diff --git a/src/use-lunatic/commons/variables/lunatic-variables-store.ts b/src/use-lunatic/commons/variables/lunatic-variables-store.ts index 90d48349b..f4fa43754 100644 --- a/src/use-lunatic/commons/variables/lunatic-variables-store.ts +++ b/src/use-lunatic/commons/variables/lunatic-variables-store.ts @@ -33,6 +33,8 @@ export type EventArgs = { value: unknown; /** Iteration changed (for array). */ iteration?: IterationLevel | undefined; + /** When resize an array directly with only one handleChange (remove one line in tableLoop) */ + removedIndex?: number; /** What triggered this change. */ cause?: 'resizing' | 'cleaning'; /** Extra sent when setting the variable. */ @@ -123,7 +125,7 @@ export class LunaticVariablesStore { public set( name: string, value: unknown, - args: Pick = {} + args: Pick = {} ): LunaticVariable { if (!this.dictionary.has(name)) { this.dictionary.set( diff --git a/src/use-lunatic/reducer/commons/index.ts b/src/use-lunatic/reducer/commons/index.ts index 80b2cadf0..5bedc64fa 100644 --- a/src/use-lunatic/reducer/commons/index.ts +++ b/src/use-lunatic/reducer/commons/index.ts @@ -1,2 +1 @@ -export { default as resizeArrayVariable } from './resize-array-variable'; export * from './validate-condition-filter'; diff --git a/src/use-lunatic/reducer/commons/resize-array-variable.ts b/src/use-lunatic/reducer/commons/resize-array-variable.ts deleted file mode 100644 index ac7765626..000000000 --- a/src/use-lunatic/reducer/commons/resize-array-variable.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Cast the variable into an array and adjust the length if necessary - */ -function resizeArrayVariable( - array: unknown, - length: number, - defaultValue?: T -): T[] { - if (!Array.isArray(array)) { - // create the array - return new Array(length).fill(defaultValue); - } else if (array.length !== length) { - // renew array end keep previous values - return new Array(length).fill(defaultValue).reduce(function ( - step, - current, - index - ) { - if (index < array.length) { - return [...step, array[index]]; - } - return [...step, current]; - }, []); - } - return array; -} - -export default resizeArrayVariable; diff --git a/src/use-lunatic/type.ts b/src/use-lunatic/type.ts index 13cf8a558..b7cbf119e 100644 --- a/src/use-lunatic/type.ts +++ b/src/use-lunatic/type.ts @@ -349,5 +349,6 @@ export type LunaticChangesHandler = ( name: string; value: any; iteration?: number[]; + removedIndex?: number; }[] ) => void; diff --git a/src/utils/array.spec.ts b/src/utils/array.spec.ts index aa60a0194..ed8302ec8 100644 --- a/src/utils/array.spec.ts +++ b/src/utils/array.spec.ts @@ -1,5 +1,10 @@ import { describe, it, expect } from 'vitest'; -import { firstValueItem, resizeArray, setAtIndex } from './array'; +import { + firstValueItem, + resizeArray, + resizeDownArrayWithIndex, + setAtIndex, +} from './array'; describe('array', () => { describe('resizeArray()', () => { @@ -45,4 +50,16 @@ describe('array', () => { expect(firstValueItem([null, 1, 2])).toBe(1); expect(firstValueItem([null, undefined, false])).toBe(false); }); + describe('resizeDownArrayWithIndex()', () => { + it('should remove an element of array', () => { + expect(resizeDownArrayWithIndex([1, 2, 3, 4], 2)).toEqual([1, 2, 4]); + expect(resizeDownArrayWithIndex([1, 2, 3, 4], 0)).toEqual([2, 3, 4]); + expect(resizeDownArrayWithIndex([1, 2, 3, 4], 3)).toEqual([1, 2, 3]); + }); + + it('should not remove element (out of index)', () => { + expect(resizeDownArrayWithIndex([1, 2, 3, 4], -1)).toEqual([1, 2, 3, 4]); + expect(resizeDownArrayWithIndex([1, 2, 3, 4], 4)).toEqual([1, 2, 3, 4]); + }); + }); }); diff --git a/src/utils/array.ts b/src/utils/array.ts index 4dbc0d12b..1807f43a0 100644 --- a/src/utils/array.ts +++ b/src/utils/array.ts @@ -51,6 +51,9 @@ export function getAtIndex(arr: unknown, indexes: number[]): unknown { return current; } +/** + * Cast the variable into an array and adjust the length if necessary + */ export function resizeArray( array: unknown, newLength: number, @@ -63,14 +66,29 @@ export function resizeArray( if (array.length === newLength) { return array; } - return new Array(newLength).fill(defaultValue ?? null).map(function ( - value, + return new Array(newLength).fill(defaultValue ?? null).reduce(function ( + step, + current, index ) { - return index < array.length ? array[index] : value; + if (index < array.length) { + return [...step, array[index]]; + } + return [...step, current]; }, []); } +export function resizeDownArrayWithIndex( + array: T[], + removedIndex: number +): T[] { + // the removedIndex is not in array + if (0 > removedIndex || array.length <= removedIndex) { + return array; + } + return array.filter((_, i) => i !== removedIndex); +} + /** * Return the first non-null/undefined value of an array */