diff --git a/e2e/pairwise.spec.ts b/e2e/pairwise.spec.ts index 0414d01ca..91876c034 100644 --- a/e2e/pairwise.spec.ts +++ b/e2e/pairwise.spec.ts @@ -21,8 +21,7 @@ for (const [label, story] of stories) { await page.getByRole('button', { name: 'Next' }).click(); await page.getByText('Sa fille, son fils').click(); await expect(page.getByText('Sa mère, son père')).toBeVisible(); - await gotoNextPage(page, 2); - await expect(page.getByText('END')).toBeVisible(); + await gotoNextPage(page); await expectLunaticData(page, 'COLLECTED.LINKS.COLLECTED', [ [null, '3', null, null], ['2', null, null, null], diff --git a/src/components/index.ts b/src/components/index.ts index 29db8b9cb..e58adbd54 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -20,7 +20,7 @@ export { default as Switch } from './switch'; export { default as Textarea } from './textarea'; export { default as SuggesterLoaderWidget } from './suggester-loader-widget'; export { default as Roundabout } from './roundabout'; -export { default as Table } from './table'; +export { LunaticTable as Table } from './table/lunatic-table'; export { LunaticComponentSet as ComponentSet } from './component-set/lunatic-component-set'; export { default as Duration } from './duration'; export { Summary } from './summary'; diff --git a/src/components/loop/block-for-loop.tsx b/src/components/loop/block-for-loop.tsx index 20497ae0c..5a0c8ccf9 100644 --- a/src/components/loop/block-for-loop.tsx +++ b/src/components/loop/block-for-loop.tsx @@ -27,8 +27,9 @@ export const BlockForLoop = createCustomizableLunaticField< iterations, getComponents, } = props; - const min = lines?.min; - const max = lines?.max; + const min = lines?.min ?? 0; + const max = lines?.max ?? Infinity; + const canControlRows = min !== max; const [nbRows, setNbRows] = useState(() => { return Math.max(iterations, min); @@ -60,6 +61,7 @@ export const BlockForLoop = createCustomizableLunaticField< if (nbRows <= 0) { return null; } + return ( <> @@ -72,7 +74,7 @@ export const BlockForLoop = createCustomizableLunaticField< /> ))} - {Number.isInteger(min) && Number.isInteger(max) && min !== max && ( + {canControlRows && ( <> {label || D.DEFAULT_BUTTON_ADD} diff --git a/src/components/loop/roster-for-loop/roster-for-loop.tsx b/src/components/loop/roster-for-loop/roster-for-loop.tsx index 99f7e4caf..b6e44ca3c 100644 --- a/src/components/loop/roster-for-loop/roster-for-loop.tsx +++ b/src/components/loop/roster-for-loop/roster-for-loop.tsx @@ -9,7 +9,7 @@ import { LoopButton } from '../loop-button'; import D from '../../../i18n'; import type { LunaticComponentProps } from '../../type'; import { Table, Tbody, Td, Tr } from '../../commons/components/html-table'; -import Header from '../../table/header'; +import { TableHeader } from '../../table/table-header'; import { times } from '../../../utils/array'; import { LunaticComponents } from '../../lunatic-components'; @@ -65,7 +65,7 @@ export const RosterForLoop = createCustomizableLunaticField< -
+
{times(nbRows, (n) => ( diff --git a/src/components/lunatic-components.tsx b/src/components/lunatic-components.tsx index d1cd88684..286bba655 100644 --- a/src/components/lunatic-components.tsx +++ b/src/components/lunatic-components.tsx @@ -1,23 +1,27 @@ import { Fragment, - useEffect, - useRef, type PropsWithChildren, + type ReactElement, type ReactNode, + useEffect, + useRef, } from 'react'; import * as lunaticComponents from './index'; import type { FilledLunaticComponentProps } from '../use-lunatic/commons/fill-components/fill-components'; +import { hasComponentType } from '../use-lunatic/commons/component'; type Props> = { // List of components to display (coming from getComponents) - components: FilledLunaticComponentProps[]; + components: (FilledLunaticComponentProps | ReactElement)[]; // Key that trigger autofocus when it changes (pageTag) autoFocusKey?: string; // Returns the list of extra props to add to components componentProps?: (component: FilledLunaticComponentProps) => T; // Add additional wrapper around each component wrapper?: ( - props: PropsWithChildren + props: PropsWithChildren< + FilledLunaticComponentProps & T & { index: number } + > ) => ReactNode; }; @@ -54,16 +58,28 @@ export function LunaticComponents>({ ref={WrapperComponent === Fragment ? undefined : wrapperRef} > {components.map((component, k) => { - const props = { - ...component, - ...componentProps?.(component), - }; + if (hasComponentType(component)) { + const props = { + ...component, + ...componentProps?.(component), + }; + return ( + + {wrapper({ + children: , + index: k, + ...props, + })} + + ); + } + // In some case (table for instance) we have static component that only have a label (no componentType) return ( - + {wrapper({ - children: , - ...props, - })} + children: component, + index: k, + } as any)} ); })} diff --git a/src/components/table/cell.tsx b/src/components/table/cell.tsx deleted file mode 100644 index f6d369182..000000000 --- a/src/components/table/cell.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { OrchestratedComponent } from '../commons'; -import { Td } from '../commons/components/html-table'; -import type { LunaticBaseProps } from '../type'; -import type { - LunaticComponentDefinition, - LunaticExpression, -} from '../../use-lunatic/type'; - -function collecteResponseValue( - response: unknown, - value?: Record -): unknown { - if ( - value && - typeof response === 'object' && - response && - 'name' in response && - typeof response.name === 'string' && - response.name in value - ) { - return value[response.name]; - } - - return undefined; -} - -function collecteArrayResponseValue( - responses: unknown[], - value?: Record -): unknown[] { - const [response, ...rest] = responses; - - if (response) { - return [ - collecteResponseValue(response, value), - collecteArrayResponseValue(rest, value), - ]; - } - return []; -} - -function collecteValue( - component: LunaticComponentDefinition, - value?: Record -) { - if ('responses' in component && Array.isArray(component.responses)) { - return collecteArrayResponseValue( - component.responses.map((v) => v.response), - value - ); - } - if ('response' in component) { - return collecteResponseValue(component.response, value); - } - return {}; -} - -type Props = { - content: - | LunaticComponentDefinition - | { label: LunaticExpression; rowspan?: number; colspan?: number }; - id: string; - executeExpression: LunaticBaseProps['executeExpression']; - iteration?: number; - value: Record; - row?: string | number; - index?: string | number; - handleChange: LunaticBaseProps['handleChange']; - errors?: LunaticBaseProps['errors']; -}; -function Cell({ - content, - id, - executeExpression, - iteration, - value, - row, - index, - handleChange, - errors, -}: Props) { - if ('componentType' in content) { - const valueField = collecteValue(content, value); - return ( - - ); - } - - const getLabel = () => { - try { - return executeExpression(content.label, { iteration }); - } catch (e) { - return (e as any).toString(); - } - }; - - return ( - - ); -} - -export default Cell; diff --git a/src/components/table/index.ts b/src/components/table/index.ts deleted file mode 100644 index 998591d12..000000000 --- a/src/components/table/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from './lunatic-table'; diff --git a/src/components/table/lunatic-table.tsx b/src/components/table/lunatic-table.tsx index ba20eddff..381787fef 100644 --- a/src/components/table/lunatic-table.tsx +++ b/src/components/table/lunatic-table.tsx @@ -1,19 +1,16 @@ -import { Table, Tbody } from '../commons/components/html-table'; -import Header from './header'; +import { Table, Tbody, Td, Tr } from '../commons/components/html-table'; import LunaticComponent from '../commons/components/lunatic-component-with-label'; -import TableOrchestrator from './table-orchestrator'; import type { LunaticComponentProps } from '../type'; import { Errors } from '../commons'; +import { LunaticComponents } from '../lunatic-components'; +import { TableHeader } from './table-header'; -function LunaticTable(props: LunaticComponentProps<'Table'>) { +export function LunaticTable(props: LunaticComponentProps<'Table'>) { const { id, handleChange, - value, body, header, - executeExpression, - iteration, errors, missing, declarations, @@ -35,16 +32,20 @@ function LunaticTable(props: LunaticComponentProps<'Table'>) { handleChange={handleChange} >
- - - {getLabel()} -
-
+
- + {body.map((row, rowIndex) => ( + + ( + + )} + /> + + ))}
+ {children} +
diff --git a/src/components/table/row.tsx b/src/components/table/row.tsx deleted file mode 100644 index 6a0219613..000000000 --- a/src/components/table/row.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Tr as HtmlTr } from '../commons/components/html-table'; -import Cell from './cell'; -import type { - LunaticComponentDefinition, - LunaticExpression, -} from '../../use-lunatic/type'; -import type { LunaticBaseProps } from '../type'; -type Props = { - components: Array< - | LunaticComponentDefinition - | { label: LunaticExpression; rowspan?: number; colspan?: number } - >; - id: string; - executeExpression: LunaticBaseProps['executeExpression']; - iteration?: number; - valueMap: Record; - rowIndex?: string | number; - handleChange: LunaticBaseProps['handleChange']; - errors?: LunaticBaseProps['errors']; -}; -function Row({ - id, - components, - executeExpression, - valueMap, - rowIndex, - iteration, - handleChange, - errors, -}: Props) { - const row = components.map(function (content, index) { - return ( - - ); - }); - return ( - - {row} - - ); -} - -export default Row; diff --git a/src/components/table/header.tsx b/src/components/table/table-header.tsx similarity index 62% rename from src/components/table/header.tsx rename to src/components/table/table-header.tsx index 0e24766c0..99dc7bda9 100644 --- a/src/components/table/header.tsx +++ b/src/components/table/table-header.tsx @@ -1,22 +1,18 @@ import { type ReactNode } from 'react'; -import { - Thead as HtmlThead, - Tr as HtmlTr, - Th as HtmlTh, -} from '../commons/components/html-table'; +import { Thead, Tr, Th } from '../commons/components/html-table'; type Props = { id?: string; header?: Array<{ label?: ReactNode; colspan?: number; rowspan?: number }>; }; -function Header({ id, header }: Props) { +export function TableHeader({ id, header }: Props) { if (!Array.isArray(header)) { return null; } const content = header.map(function ({ label, rowspan, colspan }, index) { return ( - {label} - + ); }); return ( - - + + {content} - - + + ); } - -export default Header; diff --git a/src/components/table/table-orchestrator.tsx b/src/components/table/table-orchestrator.tsx deleted file mode 100644 index 3ca2a64d6..000000000 --- a/src/components/table/table-orchestrator.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { LunaticComponentProps } from '../type'; -import Row from './row'; - -type Props = Pick< - LunaticComponentProps<'Table'>, - 'body' | 'id' | 'executeExpression' | 'value' | 'handleChange' | 'iteration' ->; -function TableOrchestrator({ - body, - id, - executeExpression, - value: valueMap, - handleChange, - iteration, -}: Props) { - if (!Array.isArray(body)) { - return null; - } - return ( - <> - {body.map(function (components, index) { - return ( - - ); - })} - - ); -} - -export default TableOrchestrator; diff --git a/src/components/type.ts b/src/components/type.ts index 1ba3a98c3..48f13058a 100644 --- a/src/components/type.ts +++ b/src/components/type.ts @@ -114,7 +114,7 @@ type ComponentPropsByType = { rowspan?: number; colspan?: number; }>; - body: Array>; + body: FilledLunaticComponentProps[][]; executeExpression: LunaticState['executeExpression']; iteration: LunaticState['pager']['iteration']; }; diff --git a/src/use-lunatic/commons/component.ts b/src/use-lunatic/commons/component.ts index ca92a38a1..d34868c08 100644 --- a/src/use-lunatic/commons/component.ts +++ b/src/use-lunatic/commons/component.ts @@ -1,4 +1,5 @@ import type { ReactNode } from 'react'; +import type { LunaticComponentDefinition } from '../type'; export function hasResponse( component: unknown @@ -22,3 +23,25 @@ export function hasResponses(component: unknown): component is { !!component && typeof component === 'object' && 'responses' in component ); } + +export function hasBody(component: unknown): component is { + body: LunaticComponentDefinition<'Table'>['body']; +} { + return ( + !!component && + typeof component === 'object' && + 'body' in component && + Array.isArray(component.body) + ); +} + +export function hasComponentType( + component: unknown +): component is { componentType: string } { + return ( + !!component && + typeof component === 'object' && + 'componentType' in component && + typeof component.componentType === 'string' + ); +} diff --git a/src/use-lunatic/commons/fill-components/fill-component-value.ts b/src/use-lunatic/commons/fill-components/fill-component-value.ts index 38ec93127..fe8b6770d 100644 --- a/src/use-lunatic/commons/fill-components/fill-component-value.ts +++ b/src/use-lunatic/commons/fill-components/fill-component-value.ts @@ -1,6 +1,5 @@ import type { LunaticComponentDefinition, LunaticState } from '../../type'; import { hasResponse, hasResponses } from '../component'; -import { string } from 'prop-types'; import { isNumber } from '../../../utils/number'; export type FilledProps = { value?: unknown }; diff --git a/src/use-lunatic/commons/fill-components/fill-components.ts b/src/use-lunatic/commons/fill-components/fill-components.ts index 251ac863a..20a18a745 100644 --- a/src/use-lunatic/commons/fill-components/fill-components.ts +++ b/src/use-lunatic/commons/fill-components/fill-components.ts @@ -52,15 +52,15 @@ function compose(...fill: Function[]) { * * Force typing for this function since it's doo dynamic */ -const fillComponent = compose( +export const fillComponent = compose( fillFromState, fillComponentExpressions, fillPagination, fillComponentValue, fillMissingResponse, fillManagement, - fillSpecificExpressions, - fillIterations + fillIterations, + fillSpecificExpressions ) as ( component: LunaticComponentDefinition, state: LunaticState diff --git a/src/use-lunatic/commons/fill-components/fill-specific-expression.ts b/src/use-lunatic/commons/fill-components/fill-specific-expression.ts index ff0dca1aa..52136bd7c 100644 --- a/src/use-lunatic/commons/fill-components/fill-specific-expression.ts +++ b/src/use-lunatic/commons/fill-components/fill-specific-expression.ts @@ -1,7 +1,8 @@ import type { LunaticComponentDefinition, LunaticState } from '../../type'; import { type DeepTranslateExpression } from './fill-component-expressions'; -import fillComponents from './fill-components'; -import { setAtIndex } from '../../../utils/array'; +import fillComponents, { fillComponent } from './fill-components'; +import { hasComponentType } from '../component'; +import { getVTLCompatibleValue } from '../../../utils/vtl'; /** * Fill props for roundabout @@ -69,7 +70,7 @@ function fillChildComponentsWithIteration( } /** - * Fill child components for nested component type + * For pairwise, inject a method to retrieve component at a specific iteration combination */ function fillPairwise( component: DeepTranslateExpression< @@ -109,6 +110,26 @@ function fillPairwise( }; } +/** + * For pairwise, inject a method to retrieve component at a specific iteration combination + */ +function fillTable( + component: DeepTranslateExpression>, + state: LunaticState +) { + return { + ...component, + body: component.body.map((row) => + row.map((component) => { + if (hasComponentType(component)) { + return fillComponent(component, state); + } + return state.executeExpression(getVTLCompatibleValue(component.label)); + }) + ), + }; +} + /** * Fill component specific props (RoundAbout for instance) */ @@ -126,6 +147,8 @@ function fillSpecificExpressions( return fillChildComponentsWithIteration(component, state); case 'PairwiseLinks': return fillPairwise(component, state); + case 'Table': + return fillTable(component, state); default: return component; } diff --git a/src/use-lunatic/commons/variables/behaviours/resizing-behaviour.ts b/src/use-lunatic/commons/variables/behaviours/resizing-behaviour.ts index bffb1575e..0df7b6bcf 100644 --- a/src/use-lunatic/commons/variables/behaviours/resizing-behaviour.ts +++ b/src/use-lunatic/commons/variables/behaviours/resizing-behaviour.ts @@ -41,9 +41,7 @@ export function resizingBehaviour( 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), { - iteration: e.detail.iteration, - }); + store.set(variableName, resizeArrayVariable(value, newSize, null)); } } }); @@ -72,6 +70,6 @@ function resizePairwise( xSize, new Array(ySize).fill(null) ); - store.set(variable, resizedValue, { iteration: args.iteration }); + store.set(variable, resizedValue); }); } diff --git a/src/use-lunatic/commons/variables/get-questionnaire-data.ts b/src/use-lunatic/commons/variables/get-questionnaire-data.ts index 522589c98..c87519aee 100644 --- a/src/use-lunatic/commons/variables/get-questionnaire-data.ts +++ b/src/use-lunatic/commons/variables/get-questionnaire-data.ts @@ -21,6 +21,10 @@ export function getQuestionnaireData( >, }; + if (!variables) { + return {}; + } + for (const variable of variables) { // Skip calculated value if necessary if (variable.variableType === 'CALCULATED' && !withCalculated) { diff --git a/src/use-lunatic/commons/variables/lunatic-variables-store.ts b/src/use-lunatic/commons/variables/lunatic-variables-store.ts index 8f70e257c..fb63d2d7c 100644 --- a/src/use-lunatic/commons/variables/lunatic-variables-store.ts +++ b/src/use-lunatic/commons/variables/lunatic-variables-store.ts @@ -34,6 +34,9 @@ export class LunaticVariablesStore { public static makeFromSource(source: LunaticSource, data: LunaticData) { const store = new LunaticVariablesStore(); + if (!source.variables) { + return store; + } const initialValues = Object.fromEntries( source.variables.map((variable) => [ variable.name, diff --git a/src/use-lunatic/reducer/reduce-handle-change.ts b/src/use-lunatic/reducer/reduce-handle-change.ts index 2eb70ef57..a6d60f7e3 100644 --- a/src/use-lunatic/reducer/reduce-handle-change.ts +++ b/src/use-lunatic/reducer/reduce-handle-change.ts @@ -9,8 +9,13 @@ export function reduceHandleChange( state: LunaticState, action: ActionHandleChange ): LunaticState { + let iteration = action.payload.iteration; + // Resolve iteration from pager for loops + if (!iteration && isNumber(state.pager.iteration)) { + iteration = [state.pager.iteration]; + } state.updateBindings(action.payload.name, action.payload.value, { - iteration: action.payload.iteration, + iteration, }); return { diff --git a/src/use-lunatic/type-source.ts b/src/use-lunatic/type-source.ts index 5d0fd61c5..cfe1110bf 100644 --- a/src/use-lunatic/type-source.ts +++ b/src/use-lunatic/type-source.ts @@ -1,3 +1,5 @@ +import type { LunaticComponentDefinition } from './type'; + /** * Types used for source data (lunatic models and data.json) */ @@ -172,24 +174,7 @@ export type ComponentRosterForLoopType = { colspan?: number; rowspan?: number; }[]; - body: { - label?: LabelType; - value?: string; - format?: string; - dateFormat?: string; - unit?: string; - options: { value: string; label: LabelType }[]; - response: ResponseType; - bindingDependencies: string[]; - componentType?: ComponentTypeEnum; - maxLength?: number; - min?: number; - max?: number; - decimals?: number; - colspan?: number; - rowspan?: number; - id?: string; - }[]; + body: ({ label: LabelType } | ComponentType)[][]; positioning: 'HORIZONTAL'; };