From 52ccc870ec8f346dc8425693ac77f20514c80182 Mon Sep 17 00:00:00 2001 From: Filippa Wallden <143729834+walldenfilippa@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:30:50 +0100 Subject: [PATCH 01/59] Feature/list component with checkboxes (#2926) Co-authored-by: Johanne Lie Co-authored-by: adamhaeger --- src/layout/List/ListComponent.module.css | 39 ++++- src/layout/List/ListComponent.tsx | 198 +++++++++++++++++------ src/layout/List/ListSummary.tsx | 66 +++++++- src/layout/List/config.ts | 14 ++ src/layout/List/index.tsx | 43 +++-- 5 files changed, 290 insertions(+), 70 deletions(-) diff --git a/src/layout/List/ListComponent.module.css b/src/layout/List/ListComponent.module.css index 623837b7ac..b86de19770 100644 --- a/src/layout/List/ListComponent.module.css +++ b/src/layout/List/ListComponent.module.css @@ -20,7 +20,7 @@ height: 60px; } -.radio { +.toggleControl { align-content: center; justify-content: center; } @@ -47,37 +47,60 @@ justify-content: center; } -.mobileRadioGroup > div { +.mobileGroup > div { display: flex; flex-direction: column; } -.mobileRadioGroup > div :hover { +.mobileGroup > div :hover { background-color: var(--fds-colors-grey-100); } -.mobileRadio { +.mobile { border-top: 1px solid var(--fds-colors-grey-200); padding: var(--fds-spacing-4); gap: var(--fds-spacing-8); } -.mobileRadio input:checked:hover { +.mobile input:checked:hover { background-color: var(--fds-radio-border-color); } -.mobileRadio label { +.mobile label { width: 100%; } -.mobileRadio span { +.mobile span { display: flex; flex-direction: column; gap: var(--fds-spacing-4); } -.mobileRadio label div { +.mobile label div { display: flex; flex-direction: column; gap: var(--fds-spacing-2); } + +.listContainer { + display: flex; + flex-direction: column; + gap: var(--fds-spacing-2); +} + +.headerContainer { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +} + +.header { + font-size: 1.125rem; + align-content: center; +} + +.editButton { + margin-left: auto; + min-width: unset; +} diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index d4dc08f98f..925a19ca23 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -2,8 +2,9 @@ import React, { useState } from 'react'; import type { AriaAttributes } from 'react'; import { Pagination as AltinnPagination } from '@altinn/altinn-design-system'; -import { Heading, Radio, Table } from '@digdir/designsystemet-react'; +import { Checkbox, Heading, Radio, Table } from '@digdir/designsystemet-react'; import cn from 'classnames'; +import { v4 as uuidv4 } from 'uuid'; import type { DescriptionText } from '@altinn/altinn-design-system/dist/types/src/components/Pagination/Pagination'; import { Description } from 'src/components/form/Description'; @@ -11,6 +12,8 @@ import { RadioButton } from 'src/components/form/RadioButton'; import { RequiredIndicator } from 'src/components/form/RequiredIndicator'; import { getLabelId } from 'src/components/label/Label'; import { useDataListQuery } from 'src/features/dataLists/useDataListQuery'; +import { FD } from 'src/features/formData/FormDataWrite'; +import { ALTINN_ROW_ID, DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/types'; import { useDataModelBindings } from 'src/features/formData/useDataModelBindings'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; @@ -54,16 +57,22 @@ export const ListComponent = ({ node }: IListProps) => { const { data } = useDataListQuery(filter, dataListId, secure, mapping, queryParameters); const bindings = item.dataModelBindings ?? ({} as IDataModelBindingsForList); - const { formData, setValues } = useDataModelBindings(bindings); + + const { formData, setValues } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); + const saveToList = item.dataModelBindings?.saveToList; + + const appendToList = FD.useAppendToList(); + const removeFromList = FD.useRemoveIndexFromList(); const tableHeadersToShowInMobile = Object.keys(tableHeaders).filter( (key) => !tableHeadersMobile || tableHeadersMobile.includes(key), ); - const selectedRow = - data?.listItems.find((row) => Object.keys(formData).every((key) => row[key] === formData[key])) ?? ''; + const selectedRow = !saveToList + ? (data?.listItems.find((row) => Object.keys(formData).every((key) => row[key] === formData[key])) ?? '') + : ''; - function handleRowSelect({ selectedValue }: { selectedValue: Row }) { + function handleSelectedRadioRow({ selectedValue }: { selectedValue: Row }) { const next: Row = {}; for (const binding of Object.keys(bindings)) { next[binding] = selectedValue[binding]; @@ -75,46 +84,122 @@ export const ListComponent = ({ node }: IListProps) => { return JSON.stringify(selectedRow) === JSON.stringify(row); } + function isRowChecked(row: Row): boolean { + return (formData?.saveToList as Row[]).some((selectedRow) => + Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]), + ); + } + const title = item.textResourceBindings?.title; const description = item.textResourceBindings?.description; + const handleRowClick = (row) => { + if (saveToList) { + handleSelectedCheckboxRow(row); + } else { + handleSelectedRadioRow({ selectedValue: row }); + } + }; + + const handleSelectedCheckboxRow = (row) => { + if (!saveToList) { + return; + } + if (isRowChecked(row)) { + const index = (formData?.saveToList as Row[]).findIndex((selectedRow) => { + const { altinnRowId: _, ...rest } = selectedRow; + return Object.keys(rest).every((key) => Object.hasOwn(row, key) && row[key] === rest[key]); + }); + if (index >= 0) { + removeFromList({ + reference: saveToList, + index, + }); + } + } else { + const uuid = uuidv4(); + const next: Row = { [ALTINN_ROW_ID]: uuid }; + for (const binding of Object.keys(bindings)) { + if (binding != 'saveToList') { + next[binding] = row[binding]; + } + } + appendToList({ + reference: saveToList, + newValue: { ...next }, + }); + } + }; + + const renderListItems = (row, tableHeaders) => + tableHeadersToShowInMobile.map((key) => ( +
+ + + + {typeof row[key] === 'string' ? : row[key]} +
+ )); + if (isMobile) { return ( - - - - - } - description={description && } - className={classes.mobileRadioGroup} - value={JSON.stringify(selectedRow)} - > - {data?.listItems.map((row) => ( - handleRowSelect({ selectedValue: row })} - > - {tableHeadersToShowInMobile.map((key) => ( -
- - - - {typeof row[key] === 'string' ? : row[key]} -
- ))} -
- ))} -
+ {saveToList ? ( + + + + + } + description={description && } + className={classes.mobileCheckboxGroup} + > + {data?.listItems.map((row) => ( + handleRowClick(row)} + value={JSON.stringify(row)} + className={cn(classes.mobile)} + checked={isRowChecked(row)} + > + {renderListItems(row, tableHeaders)} + + ))} + + ) : ( + + + + + } + description={description && } + className={classes.mobileGroup} + value={JSON.stringify(selectedRow)} + > + {data?.listItems.map((row) => ( + handleSelectedRadioRow({ selectedValue: row })} + > + {renderListItems(row, tableHeaders)} + + ))} + + )} { {data?.listItems.map((row) => ( { - handleRowSelect({ selectedValue: row }); - }} + onClick={() => handleRowClick(row)} > - { - handleRowSelect({ selectedValue: row }); - }} - value={JSON.stringify(row)} - checked={isRowSelected(row)} - name={node.id} - /> + {saveToList ? ( + {}} + value={JSON.stringify(row)} + checked={isRowChecked(row)} + name={node.id} + /> + ) : ( + { + handleSelectedRadioRow({ selectedValue: row }); + }} + value={JSON.stringify(row)} + checked={isRowSelected(row)} + name={node.id} + /> + )} {Object.keys(tableHeaders).map((key) => ( ; emptyFieldText?: string; }; +type Row = Record; export const ListSummary = ({ componentNode, isCompact, emptyFieldText }: ListComponentSummaryProps) => { const displayData = componentNode.def.useDisplayData(componentNode); const validations = useUnifiedValidationsForNode(componentNode); const errors = validationsOfSeverity(validations, 'error'); - const title = useNodeItem(componentNode, (i) => i.textResourceBindings?.title); + const title = useNodeItem( + componentNode, + (i) => i.textResourceBindings?.summaryTitle || i.textResourceBindings?.title, + ); + + const { tableHeaders, dataModelBindings } = useNodeItem(componentNode); + const { formData } = useDataModelBindings(dataModelBindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); + + const displayRows: Row[] = []; + (formData?.saveToList as Row[]).forEach((row: Row) => { + const { altinnRowId: _, ...rest } = row; + displayRows.push(rest); + }); + + if (displayRows.length > 0) { + return ( +
+
+ + + + +
+ + + + {Object.entries(tableHeaders).map(([key, value]) => ( + + + + ))} + + + + {displayRows.map((row, rowIndex) => { + const rowItem = row; + return ( + + {Object.entries(tableHeaders).map(([key]) => ( + + {rowItem[key]} + + ))} + + ); + })} + +
+
+ ); + } return ( ): JSX.Element | null { - const displayData = this.useDisplayData(targetNode); + renderSummary(props: SummaryRendererProps<'List'>): JSX.Element | null { + const displayData = this.useDisplayData(props.targetNode); return ; } @@ -112,17 +112,42 @@ export class List extends ListDef { validateDataModelBindings(ctx: LayoutValidationCtx<'List'>): string[] { const errors: string[] = []; + const allowedTypes = ['string', 'boolean', 'number']; + if (!ctx.item.dataModelBindings?.saveToList) { + for (const [binding] of Object.entries(ctx.item.dataModelBindings ?? {})) { + const [newErrors] = this.validateDataModelBindingsAny(ctx, binding, allowedTypes, false); + errors.push(...(newErrors || [])); + } + } - for (const binding of Object.keys(ctx.item.dataModelBindings ?? {})) { - const [newErrors] = this.validateDataModelBindingsAny( - ctx, - binding, - ['string', 'number', 'integer', 'boolean'], - false, - ); + const [newErrors] = this.validateDataModelBindingsAny(ctx, 'saveToList', ['array']); + if (newErrors) { errors.push(...(newErrors || [])); } + if (ctx.item.dataModelBindings?.saveToList) { + const saveToListBinding = ctx.lookupBinding(ctx.item?.dataModelBindings?.saveToList); + const items = saveToListBinding[0]?.items; + const properties = + items && !Array.isArray(items) && typeof items === 'object' && 'properties' in items + ? items.properties + : undefined; + + for (const [binding] of Object.entries(ctx.item.dataModelBindings ?? {})) { + let selectedBinding; + if (properties) { + selectedBinding = properties[binding]; + } + if (binding !== 'saveToList' && items && typeof items === 'object' && 'properties' in items) { + if (!selectedBinding) { + errors.push(`saveToList must contain a field with the same name as the field ${binding}`); + } else if (!allowedTypes.includes(selectedBinding.type)) { + errors.push(`Field ${binding} in saveToList must be of type string, number or boolean`); + } + } + } + } + return errors; } From c0f99b4a5ac2ea27ac1e09e91e0ca07e2bcb9df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Thu, 13 Feb 2025 10:17:43 +0100 Subject: [PATCH 02/59] Remove errors and bugs --- src/layout/List/ListComponent.tsx | 2 +- src/layout/List/ListSummary.tsx | 2 +- src/layout/List/config.ts | 14 -------------- src/layout/List/index.tsx | 5 ----- 4 files changed, 2 insertions(+), 21 deletions(-) diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index 97f92a6086..8c06c0dffa 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -156,7 +156,7 @@ export const ListComponent = ({ node }: IListProps) => { } description={description && } - className={classes.mobileCheckboxGroup} + //className={classes.mobileCheckboxGroup} > {data?.listItems.map((row) => ( { + (formData?.saveToList as Row[])?.forEach((row: Row) => { const { altinnRowId: _, ...rest } = row; displayRows.push(rest); }); diff --git a/src/layout/List/config.ts b/src/layout/List/config.ts index 93cf041dfa..a5e20da265 100644 --- a/src/layout/List/config.ts +++ b/src/layout/List/config.ts @@ -28,20 +28,6 @@ export const Config = new CG.component({ .addDataModelBinding( new CG.obj().optional().additionalProperties(new CG.dataModelBinding()).exportAs('IDataModelBindingsForList'), ) - .addDataModelBinding( - new CG.obj( - new CG.prop( - 'saveToList', - new CG.dataModelBinding() - .setTitle('SaveToList') - .setDescription( - 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', - ), - ), - ) - .optional() - .exportAs('IDataModelBindingsForSaveTolist'), - ) .addProperty( new CG.prop( 'tableHeaders', diff --git a/src/layout/List/index.tsx b/src/layout/List/index.tsx index c876ac0bb5..c7aee7320d 100644 --- a/src/layout/List/index.tsx +++ b/src/layout/List/index.tsx @@ -120,11 +120,6 @@ export class List extends ListDef { } } - const [newErrors] = this.validateDataModelBindingsAny(ctx, 'saveToList', ['array']); - if (newErrors) { - errors.push(...(newErrors || [])); - } - if (ctx.item.dataModelBindings?.saveToList) { const saveToListBinding = ctx.lookupBinding(ctx.item?.dataModelBindings?.saveToList); const items = saveToListBinding[0]?.items; From b23049f79193fbf2220862aeb91eeecc3da88d42 Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Thu, 13 Feb 2025 10:28:45 +0100 Subject: [PATCH 03/59] Allowing 'integer' as a number type, fixing type issues in data model path validator --- src/layout/List/index.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/layout/List/index.tsx b/src/layout/List/index.tsx index c7aee7320d..f052ac9516 100644 --- a/src/layout/List/index.tsx +++ b/src/layout/List/index.tsx @@ -1,6 +1,8 @@ import React, { forwardRef } from 'react'; import type { JSX } from 'react'; +import type { JSONSchema7Definition } from 'json-schema'; + import { evalQueryParameters } from 'src/features/options/evalQueryParameters'; import { FrontendValidationSource, ValidationMask } from 'src/features/validation'; import { ListDef } from 'src/layout/List/config.def.generated'; @@ -112,7 +114,7 @@ export class List extends ListDef { validateDataModelBindings(ctx: LayoutValidationCtx<'List'>): string[] { const errors: string[] = []; - const allowedTypes = ['string', 'boolean', 'number']; + const allowedTypes = ['string', 'boolean', 'number', 'integer']; if (!ctx.item.dataModelBindings?.saveToList) { for (const [binding] of Object.entries(ctx.item.dataModelBindings ?? {})) { const [newErrors] = this.validateDataModelBindingsAny(ctx, binding, allowedTypes, false); @@ -129,15 +131,17 @@ export class List extends ListDef { : undefined; for (const [binding] of Object.entries(ctx.item.dataModelBindings ?? {})) { - let selectedBinding; + let selectedBinding: JSONSchema7Definition | undefined; if (properties) { selectedBinding = properties[binding]; } if (binding !== 'saveToList' && items && typeof items === 'object' && 'properties' in items) { if (!selectedBinding) { errors.push(`saveToList must contain a field with the same name as the field ${binding}`); + } else if (typeof selectedBinding !== 'object' || typeof selectedBinding.type !== 'string') { + errors.push(`Field ${binding} in saveToList must be one of types ${allowedTypes.join(', ')}`); } else if (!allowedTypes.includes(selectedBinding.type)) { - errors.push(`Field ${binding} in saveToList must be of type string, number or boolean`); + errors.push(`Field ${binding} in saveToList must be one of types ${allowedTypes.join(', ')}`); } } } From 38ca4c8b8b3e167da325953eeaa6f44028cb793e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Thu, 13 Feb 2025 14:08:42 +0100 Subject: [PATCH 04/59] Fix validation message and config --- src/layout/List/config.ts | 13 +++++++++++++ src/layout/List/index.tsx | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/src/layout/List/config.ts b/src/layout/List/config.ts index a5e20da265..11a3dbbaf4 100644 --- a/src/layout/List/config.ts +++ b/src/layout/List/config.ts @@ -28,6 +28,19 @@ export const Config = new CG.component({ .addDataModelBinding( new CG.obj().optional().additionalProperties(new CG.dataModelBinding()).exportAs('IDataModelBindingsForList'), ) + .addDataModelBinding( + new CG.obj( + new CG.prop( + 'saveToList', + new CG.dataModelBinding() + .setTitle('SaveToList') + .setDescription( + 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', + ) + .optional(), + ), + ).exportAs('IDataModelBindingsForSaveTolist'), + ) .addProperty( new CG.prop( 'tableHeaders', diff --git a/src/layout/List/index.tsx b/src/layout/List/index.tsx index f052ac9516..b022e6a887 100644 --- a/src/layout/List/index.tsx +++ b/src/layout/List/index.tsx @@ -121,6 +121,10 @@ export class List extends ListDef { errors.push(...(newErrors || [])); } } + const [newErrors] = this.validateDataModelBindingsAny(ctx, 'saveToList', ['array'], false); + if (newErrors) { + errors.push(...(newErrors || [])); + } if (ctx.item.dataModelBindings?.saveToList) { const saveToListBinding = ctx.lookupBinding(ctx.item?.dataModelBindings?.saveToList); From 87c5ecefc94d74e5cfe3d6d6d3218f3726c444a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Tue, 18 Feb 2025 14:12:35 +0100 Subject: [PATCH 05/59] Cypress test for list with checkboxes and changes to support only mandatory for bound values --- src/layout/List/ListComponent.tsx | 4 +- .../e2e/integration/component-library/list.ts | 114 ++++++++++++++++++ 2 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 test/e2e/integration/component-library/list.ts diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index 8c06c0dffa..723db47e19 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -106,8 +106,8 @@ export const ListComponent = ({ node }: IListProps) => { } if (isRowChecked(row)) { const index = (formData?.saveToList as Row[]).findIndex((selectedRow) => { - const { altinnRowId: _, ...rest } = selectedRow; - return Object.keys(rest).every((key) => Object.hasOwn(row, key) && row[key] === rest[key]); + const { altinnRowId: _ } = selectedRow; + return Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]); }); if (index >= 0) { removeFromList({ diff --git a/test/e2e/integration/component-library/list.ts b/test/e2e/integration/component-library/list.ts new file mode 100644 index 0000000000..626ec3000f --- /dev/null +++ b/test/e2e/integration/component-library/list.ts @@ -0,0 +1,114 @@ +import { beforeEach } from 'mocha'; + +import { AppFrontend } from 'test/e2e/pageobjects/app-frontend'; + +const appFrontend = new AppFrontend(); + +describe('List component', () => { + beforeEach(() => { + cy.startAppInstance(appFrontend.apps.componentLibrary, { authenticationLevel: '2' }); + cy.gotoNavPage('Liste (tabell)'); + }); + it('Should be possible to select multiple rows', () => { + cy.get('input[name*="ListPage-ListWithCheckboxesComponent"]').should('have.length', 5); + + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Johanne').parent().click(); + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') + .contains('td', 'Johanne') + .parent() + .findByRole('checkbox') + .should('be.checked'); + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') + .contains('td', 'Kari') + .parent() + .findByRole('checkbox') + .should('be.checked'); + }); + + it('Should be possible to deselect rows', () => { + cy.get('input[name*="ListPage-ListWithCheckboxesComponent"]').should('have.length', 5); + + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Johanne').parent().click(); + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Johanne').parent().click(); + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') + .contains('td', 'Johanne') + .parent() + .findByRole('checkbox') + .should('not.be.checked'); + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') + .contains('td', 'Kari') + .parent() + .findByRole('checkbox') + .should('be.checked'); + }); + + it('Selections in list should apply to RepeatingGroup', () => { + cy.get('input[name*="ListPage-ListWithCheckboxesComponent"]').should('have.length', 5); + + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Johanne').parent().click(); + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Johanne').parent().click(); + + cy.get('div[data-componentid*="RepeatingGroupListWithCheckboxes"]').contains('td', 'Kari'); + }); + + it('Removing from RepeatingGroup should deselect from List', () => { + cy.get('input[name*="ListPage-ListWithCheckboxesComponent"]').should('have.length', 5); + + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); + + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') + .contains('td', 'Kari') + .parent() + .findByRole('checkbox') + .should('be.checked'); + + cy.get('div[data-componentid*="RepeatingGroupListWithCheckboxes"]') + .contains('td', 'Kari') + .parent() + .contains('td', 'Slett') + .findByRole('button') + .click(); + + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') + .contains('td', 'Kari') + .parent() + .findByRole('checkbox') + .should('not.be.checked'); + }); + + it('Deselecting in List should remove from RepeatingGroup', () => { + cy.get('input[name*="ListPage-ListWithCheckboxesComponent"]').should('have.length', 5); + + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); + + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') + .contains('td', 'Kari') + .parent() + .findByRole('checkbox') + .should('be.checked'); + + cy.get('div[data-componentid*="RepeatingGroupListWithCheckboxes"]') + .contains('td', 'Kari') + .parent() + .contains('td', 'Rediger') + .findByRole('button') + .click(); + + cy.findByRole('textbox', { name: /Surname/ }).type('Olsen'); + cy.findAllByRole('button', { name: /Lagre og lukk/ }) + .first() + .click(); + + cy.get('div[data-componentid*="RepeatingGroupListWithCheckboxes"]') + .contains('td', 'Kari') + .parent() + .contains('td', 'Olsen'); + + cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); + + cy.get('div[data-componentid*="group-RepeatingGroupListWithCheckboxes"]').should('not.exist'); + }); +}); From a1b6cf713608395f3885a4f7c91a5f2fdf701b4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Tue, 25 Feb 2025 09:14:16 +0100 Subject: [PATCH 06/59] Fix better tests --- .../e2e/integration/component-library/list.ts | 102 ++++-------------- 1 file changed, 22 insertions(+), 80 deletions(-) diff --git a/test/e2e/integration/component-library/list.ts b/test/e2e/integration/component-library/list.ts index 626ec3000f..ba0791652a 100644 --- a/test/e2e/integration/component-library/list.ts +++ b/test/e2e/integration/component-library/list.ts @@ -10,105 +10,47 @@ describe('List component', () => { cy.gotoNavPage('Liste (tabell)'); }); it('Should be possible to select multiple rows', () => { - cy.get('input[name*="ListPage-ListWithCheckboxesComponent"]').should('have.length', 5); - - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Johanne').parent().click(); - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') - .contains('td', 'Johanne') - .parent() - .findByRole('checkbox') - .should('be.checked'); - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') - .contains('td', 'Kari') - .parent() - .findByRole('checkbox') - .should('be.checked'); + cy.findAllByRole('cell', { name: 'Johanne' }).last().parent().click(); /*[1].parent().click();*/ + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().click(); + cy.findAllByRole('cell', { name: 'Johanne' }).last().parent().findByRole('checkbox').should('be.checked'); + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().findByRole('checkbox').should('be.checked'); }); it('Should be possible to deselect rows', () => { - cy.get('input[name*="ListPage-ListWithCheckboxesComponent"]').should('have.length', 5); - - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Johanne').parent().click(); - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Johanne').parent().click(); - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') - .contains('td', 'Johanne') - .parent() - .findByRole('checkbox') - .should('not.be.checked'); - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') - .contains('td', 'Kari') - .parent() - .findByRole('checkbox') - .should('be.checked'); + cy.findAllByRole('cell', { name: 'Johanne' }).last().parent().click(); + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().click(); + cy.findAllByRole('cell', { name: 'Johanne' }).last().parent().click(); + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().findByRole('checkbox').should('be.checked'); + cy.findAllByRole('cell', { name: 'Johanne' }).last().parent().findByRole('checkbox').should('not.be.checked'); }); it('Selections in list should apply to RepeatingGroup', () => { - cy.get('input[name*="ListPage-ListWithCheckboxesComponent"]').should('have.length', 5); + cy.findAllByRole('cell', { name: 'Johanne' }).last().parent().click(); + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().click(); + cy.findAllByRole('cell', { name: 'Johanne' }).last().parent().click(); - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Johanne').parent().click(); - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Johanne').parent().click(); - - cy.get('div[data-componentid*="RepeatingGroupListWithCheckboxes"]').contains('td', 'Kari'); + cy.findAllByRole('cell', { name: 'Kari' }).last().should('exist'); }); it('Removing from RepeatingGroup should deselect from List', () => { - cy.get('input[name*="ListPage-ListWithCheckboxesComponent"]').should('have.length', 5); - - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().click(); + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().findByRole('checkbox').should('be.checked'); - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') - .contains('td', 'Kari') - .parent() - .findByRole('checkbox') - .should('be.checked'); + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().contains('td', 'Slett').findByRole('button').click(); - cy.get('div[data-componentid*="RepeatingGroupListWithCheckboxes"]') - .contains('td', 'Kari') - .parent() - .contains('td', 'Slett') - .findByRole('button') - .click(); - - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') - .contains('td', 'Kari') - .parent() - .findByRole('checkbox') - .should('not.be.checked'); + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().findByRole('checkbox').should('not.be.checked'); }); it('Deselecting in List should remove from RepeatingGroup', () => { - cy.get('input[name*="ListPage-ListWithCheckboxesComponent"]').should('have.length', 5); - - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); - - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]') - .contains('td', 'Kari') - .parent() - .findByRole('checkbox') - .should('be.checked'); - - cy.get('div[data-componentid*="RepeatingGroupListWithCheckboxes"]') - .contains('td', 'Kari') - .parent() - .contains('td', 'Rediger') - .findByRole('button') - .click(); - + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().click(); + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().findByRole('checkbox').should('be.checked'); + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().contains('td', 'Rediger').findByRole('button').click(); cy.findByRole('textbox', { name: /Surname/ }).type('Olsen'); cy.findAllByRole('button', { name: /Lagre og lukk/ }) .first() .click(); - - cy.get('div[data-componentid*="RepeatingGroupListWithCheckboxes"]') - .contains('td', 'Kari') - .parent() - .contains('td', 'Olsen'); - - cy.get('div[data-componentid*="ListPage-ListWithCheckboxesComponent"]').contains('td', 'Kari').parent().click(); - + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().contains('td', 'Olsen'); + cy.findAllByRole('cell', { name: 'Kari' }).last().parent().click(); cy.get('div[data-componentid*="group-RepeatingGroupListWithCheckboxes"]').should('not.exist'); }); }); From 12c0e8673dc2bd054a8338f251602001eac9c5db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Tue, 25 Feb 2025 10:09:23 +0100 Subject: [PATCH 07/59] Refactor and cleanup --- src/layout/List/ListComponent.tsx | 1 - src/layout/List/index.tsx | 14 +++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index 723db47e19..4fc3576920 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -156,7 +156,6 @@ export const ListComponent = ({ node }: IListProps) => { } description={description && } - //className={classes.mobileCheckboxGroup} > {data?.listItems.map((row) => ( ): string[] { const errors: string[] = []; const allowedTypes = ['string', 'boolean', 'number', 'integer']; - if (!ctx.item.dataModelBindings?.saveToList) { - for (const [binding] of Object.entries(ctx.item.dataModelBindings ?? {})) { + + const dataModelBindings = ctx.item.dataModelBindings ?? {}; + + if (!dataModelBindings?.saveToList) { + for (const [binding] of Object.entries(dataModelBindings ?? {})) { const [newErrors] = this.validateDataModelBindingsAny(ctx, binding, allowedTypes, false); errors.push(...(newErrors || [])); } } + const [newErrors] = this.validateDataModelBindingsAny(ctx, 'saveToList', ['array'], false); if (newErrors) { errors.push(...(newErrors || [])); } - if (ctx.item.dataModelBindings?.saveToList) { - const saveToListBinding = ctx.lookupBinding(ctx.item?.dataModelBindings?.saveToList); + if (dataModelBindings?.saveToList) { + const saveToListBinding = ctx.lookupBinding(dataModelBindings?.saveToList); const items = saveToListBinding[0]?.items; const properties = items && !Array.isArray(items) && typeof items === 'object' && 'properties' in items ? items.properties : undefined; - for (const [binding] of Object.entries(ctx.item.dataModelBindings ?? {})) { + for (const [binding] of Object.entries(dataModelBindings ?? {})) { let selectedBinding: JSONSchema7Definition | undefined; if (properties) { selectedBinding = properties[binding]; From ba657ee13fcde039ced2cc902d2218157e7b575a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Tue, 25 Feb 2025 10:39:18 +0100 Subject: [PATCH 08/59] Refactor and cleanup --- src/layout/List/ListComponent.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index 4fc3576920..fc986359da 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -92,7 +92,7 @@ export const ListComponent = ({ node }: IListProps) => { const title = item.textResourceBindings?.title; const description = item.textResourceBindings?.description; - const handleRowClick = (row) => { + const handleRowClick = (row: Row) => { if (saveToList) { handleSelectedCheckboxRow(row); } else { @@ -100,7 +100,7 @@ export const ListComponent = ({ node }: IListProps) => { } }; - const handleSelectedCheckboxRow = (row) => { + const handleSelectedCheckboxRow = (row: Row) => { if (!saveToList) { return; } @@ -119,7 +119,7 @@ export const ListComponent = ({ node }: IListProps) => { const uuid = uuidv4(); const next: Row = { [ALTINN_ROW_ID]: uuid }; for (const binding of Object.keys(bindings)) { - if (binding != 'saveToList') { + if (binding !== 'saveToList') { next[binding] = row[binding]; } } @@ -130,7 +130,7 @@ export const ListComponent = ({ node }: IListProps) => { } }; - const renderListItems = (row, tableHeaders) => + const renderListItems = (row: Row, tableHeaders) => tableHeadersToShowInMobile.map((key) => (
@@ -145,7 +145,6 @@ export const ListComponent = ({ node }: IListProps) => { {saveToList ? ( Date: Tue, 25 Feb 2025 10:40:52 +0100 Subject: [PATCH 09/59] Refactor and cleanup --- src/layout/List/ListComponent.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index fc986359da..841861a27a 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -170,7 +170,6 @@ export const ListComponent = ({ node }: IListProps) => { ) : ( Date: Tue, 25 Feb 2025 14:11:23 +0100 Subject: [PATCH 10/59] Fix caption on listSummary --- src/layout/List/ListComponent.module.css | 5 ++--- src/layout/List/ListSummary.tsx | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/layout/List/ListComponent.module.css b/src/layout/List/ListComponent.module.css index 3b62511c10..6599747a8f 100644 --- a/src/layout/List/ListComponent.module.css +++ b/src/layout/List/ListComponent.module.css @@ -94,9 +94,8 @@ width: 100%; } -.header { - font-size: 1.125rem; - align-content: center; +.tableCaption { + text-align: left; } .editButton { diff --git a/src/layout/List/ListSummary.tsx b/src/layout/List/ListSummary.tsx index 4b9048cfd2..a4e5013788 100644 --- a/src/layout/List/ListSummary.tsx +++ b/src/layout/List/ListSummary.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { Table } from '@digdir/designsystemet-react'; +import { Heading, Table } from '@digdir/designsystemet-react'; import { DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/types'; import { useDataModelBindings } from 'src/features/formData/useDataModelBindings'; @@ -42,9 +42,6 @@ export const ListSummary = ({ componentNode, isCompact, emptyFieldText }: ListCo return (
- - -
+ {title && ( + + )} {Object.entries(tableHeaders).map(([key, value]) => ( From 57a1f779161e8c35dd60f124b33f04ff385185eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Tue, 25 Feb 2025 14:15:30 +0100 Subject: [PATCH 11/59] Fix caption on listSummary --- src/layout/List/ListSummary.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/layout/List/ListSummary.tsx b/src/layout/List/ListSummary.tsx index a4e5013788..f905df9038 100644 --- a/src/layout/List/ListSummary.tsx +++ b/src/layout/List/ListSummary.tsx @@ -32,10 +32,9 @@ export const ListSummary = ({ componentNode, isCompact, emptyFieldText }: ListCo const { tableHeaders, dataModelBindings } = useNodeItem(componentNode); const { formData } = useDataModelBindings(dataModelBindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); - const displayRows: Row[] = []; - (formData?.saveToList as Row[])?.forEach((row: Row) => { + const displayRows = (formData?.saveToList as Row[]).map((row: Row) => { const { altinnRowId: _, ...rest } = row; - displayRows.push(rest); + return rest; }); if (displayRows.length > 0) { From 5ac98836f5617afbcb561208ede5a33d0df3e389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Tue, 25 Feb 2025 14:26:39 +0100 Subject: [PATCH 12/59] Refactor for readability --- src/layout/List/ListComponent.tsx | 116 ++++++++++++++++-------------- 1 file changed, 61 insertions(+), 55 deletions(-) diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index 841861a27a..a8d69ecd33 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import type { AriaAttributes } from 'react'; +import type { AriaAttributes, ReactNode } from 'react'; import { Checkbox, Heading, Radio, Table } from '@digdir/designsystemet-react'; import cn from 'classnames'; @@ -130,7 +130,7 @@ export const ListComponent = ({ node }: IListProps) => { } }; - const renderListItems = (row: Row, tableHeaders) => + const renderListItems = (row: Row, tableHeaders: { [x: string]: string | undefined }) => tableHeadersToShowInMobile.map((key) => (
@@ -139,63 +139,69 @@ export const ListComponent = ({ node }: IListProps) => { {typeof row[key] === 'string' ? : row[key]}
)); + let options: ReactNode; + if (saveToList) { + options = ( + + + + + } + description={description && } + > + {data?.listItems.map((row) => ( + handleRowClick(row)} + value={JSON.stringify(row)} + className={cn(classes.mobile)} + checked={isRowChecked(row)} + > + {renderListItems(row, tableHeaders)} + + ))} + + ); + } else { + options = ( + + + + + } + description={description && } + className={classes.mobileGroup} + value={JSON.stringify(selectedRow)} + > + {data?.listItems.map((row) => ( + handleSelectedRadioRow({ selectedValue: row })} + > + {renderListItems(row, tableHeaders)} + + ))} + + ); + } if (isMobile) { return ( - {saveToList ? ( - - - - - } - description={description && } - > - {data?.listItems.map((row) => ( - handleRowClick(row)} - value={JSON.stringify(row)} - className={cn(classes.mobile)} - checked={isRowChecked(row)} - > - {renderListItems(row, tableHeaders)} - - ))} - - ) : ( - - - - - } - description={description && } - className={classes.mobileGroup} - value={JSON.stringify(selectedRow)} - > - {data?.listItems.map((row) => ( - handleSelectedRadioRow({ selectedValue: row })} - > - {renderListItems(row, tableHeaders)} - - ))} - - )} + {options} Date: Wed, 26 Feb 2025 08:55:52 +0100 Subject: [PATCH 13/59] Fix error with tests after refactoring --- src/layout/List/ListSummary.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/layout/List/ListSummary.tsx b/src/layout/List/ListSummary.tsx index f905df9038..1f8e48d03d 100644 --- a/src/layout/List/ListSummary.tsx +++ b/src/layout/List/ListSummary.tsx @@ -32,12 +32,12 @@ export const ListSummary = ({ componentNode, isCompact, emptyFieldText }: ListCo const { tableHeaders, dataModelBindings } = useNodeItem(componentNode); const { formData } = useDataModelBindings(dataModelBindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); - const displayRows = (formData?.saveToList as Row[]).map((row: Row) => { + const displayRows = (formData?.saveToList as Row[])?.map((row: Row) => { const { altinnRowId: _, ...rest } = row; return rest; }); - if (displayRows.length > 0) { + if (displayRows?.length > 0) { return (
From 82908af8a6e1378b12ca2f41419c0946ac138bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Wed, 26 Feb 2025 08:56:11 +0100 Subject: [PATCH 14/59] Fix error with tests after refactoring --- src/layout/List/ListSummary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layout/List/ListSummary.tsx b/src/layout/List/ListSummary.tsx index 1f8e48d03d..0a2b7faf31 100644 --- a/src/layout/List/ListSummary.tsx +++ b/src/layout/List/ListSummary.tsx @@ -71,7 +71,7 @@ export const ListSummary = ({ componentNode, isCompact, emptyFieldText }: ListCo - {displayRows.map((row, rowIndex) => { + {displayRows?.map((row, rowIndex) => { const rowItem = row; return ( From f6ae3177cb201d53cb6314c46a9a82e9899e6581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Wed, 26 Feb 2025 09:22:43 +0100 Subject: [PATCH 15/59] Fix error with tests after refactoring --- src/layout/List/ListComponent.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index a8d69ecd33..e33bfcb521 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -170,6 +170,7 @@ export const ListComponent = ({ node }: IListProps) => { } else { options = ( Date: Fri, 28 Feb 2025 11:57:16 +0100 Subject: [PATCH 16/59] WIP support list of objects in checkbox component --- .../CheckboxesContainerComponent.tsx | 24 ++++++++++++++----- src/layout/Checkboxes/config.ts | 13 ++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx index a5f887cefc..95f2800248 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx @@ -15,6 +15,7 @@ import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper' import { shouldUseRowLayout } from 'src/utils/layout'; import { useNodeItem } from 'src/utils/layout/useNodeItem'; import type { PropsFromGenericComponent } from 'src/layout'; +import type { IOptionSource } from 'src/layout/common.generated'; export type ICheckboxContainerProps = PropsFromGenericComponent<'Checkboxes'>; @@ -44,6 +45,22 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC const hideLabel = overrideDisplay?.renderedInTable === true && calculatedOptions.length === 1 && !showLabelsInTable; const ariaLabel = overrideDisplay?.renderedInTable ? langAsString(textResourceBindings?.title) : undefined; + const setChecked = (isChecked: boolean, option: IOptionSource) => { + const newData = isChecked ? [...selectedValues, option.value] : selectedValues.filter((o) => o !== option.value); + setData(newData); + + /*if (item.dataModelBindings.saveToList) { + const changes = Object.entries(newData).map((entry) => ({ + reference: { + dataType: item.dataModelBindings.data.dataType, + field: `${item.dataModelBindings.data.field}[${(formData.data as []).length - 1}].${entry[0]}`, + }, + newValue: `${entry[1]}`, + })); + setMultiLeafValues({ changes }); + }*/ + }; + return ( {isFetching ? ( @@ -72,12 +89,7 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC hideLabel={hideLabel} alertOnChange={alertOnChange} checked={selectedValues.includes(option.value)} - setChecked={(isChecked) => { - const newData = isChecked - ? [...selectedValues, option.value] - : selectedValues.filter((o) => o !== option.value); - setData(newData); - }} + setChecked={(isChecked) => setChecked(isChecked, option)} /> ))} diff --git a/src/layout/Checkboxes/config.ts b/src/layout/Checkboxes/config.ts index 15f7d0bbd0..fdb989c2c0 100644 --- a/src/layout/Checkboxes/config.ts +++ b/src/layout/Checkboxes/config.ts @@ -35,6 +35,19 @@ export const Config = new CG.component({ }) .addPlugin(new OptionsPlugin({ supportsPreselection: true, type: 'multi' })) .addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple')) + .addDataModelBinding( + new CG.obj( + new CG.prop( + 'saveToList', + new CG.dataModelBinding() + .setTitle('SaveToList') + .setDescription( + 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', + ) + .optional(), + ), + ).exportAs('IDataModelBindingsForSaveTolist'), + ) .addProperty(new CG.prop('layout', CG.common('LayoutStyle').optional())) .addProperty( new CG.prop( From be55981b55fe23cca3f2a3895faa6c590fc8e3b6 Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Fri, 28 Feb 2025 12:59:54 +0100 Subject: [PATCH 17/59] checkbox saveToList WIP --- src/codegen/Common.ts | 9 ++++++ src/features/options/useGetOptions.ts | 17 +++++++++-- .../CheckboxesContainerComponent.tsx | 30 +++++++++---------- src/layout/Checkboxes/config.ts | 13 -------- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/codegen/Common.ts b/src/codegen/Common.ts index e3b42295f8..953c5a6a18 100644 --- a/src/codegen/Common.ts +++ b/src/codegen/Common.ts @@ -160,6 +160,15 @@ const common = { .setDescription('Describes the location in the data model where the component should store its metadata') .optional(), ), + new CG.prop( + 'saveToList', + new CG.dataModelBinding() + .setTitle('SaveToList') + .setDescription( + 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', + ) + .optional(), + ), ), IDataModelBindingsLikert: () => new CG.obj( diff --git a/src/features/options/useGetOptions.ts b/src/features/options/useGetOptions.ts index aa29facdee..0dfee1c062 100644 --- a/src/features/options/useGetOptions.ts +++ b/src/features/options/useGetOptions.ts @@ -79,8 +79,9 @@ export function useSetOptions( dataModelBindings: IDataModelBindingsOptionsSimple | undefined, options: IOptionInternal[], ): SetOptionsResult { - const { formData, setValue } = useDataModelBindings(dataModelBindings); + const { formData, setValue, setValues } = useDataModelBindings(dataModelBindings); const value = formData.simpleBinding ?? ''; + const saveToList = formData.saveToList; const currentValues = useMemo( () => (value && value.length > 0 ? (valueType === 'multi' ? value.split(',') : [value]) : []), @@ -98,9 +99,21 @@ export function useSetOptions( setValue('simpleBinding', values.at(0)); } else if (valueType === 'multi') { setValue('simpleBinding', values.join(',')); + if (saveToList) { + console.log('Save to list not implemented'); + setValue('saveToList', values); + // const changes = Object.entries(values).map((entry) => ({ + // reference: { + // dataType: formData.saveToList + // field: `${dataModelBindings.data.field}[${(formData.data as []).length - 1}].${entry[0]}`, + // }, + // newValue: `${entry[1]}`, + // })); + // setMultiLeafValues({ changes }); + } } }, - [setValue, valueType], + [setValue, valueType, saveToList], ); return { diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx index 95f2800248..32b04c6f37 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx @@ -5,6 +5,7 @@ import cn from 'classnames'; import { AltinnSpinner } from 'src/components/AltinnSpinner'; import { LabelContent } from 'src/components/label/LabelContent'; +import { FD } from 'src/features/formData/FormDataWrite'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import { useGetOptions } from 'src/features/options/useGetOptions'; @@ -15,18 +16,28 @@ import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper' import { shouldUseRowLayout } from 'src/utils/layout'; import { useNodeItem } from 'src/utils/layout/useNodeItem'; import type { PropsFromGenericComponent } from 'src/layout'; -import type { IOptionSource } from 'src/layout/common.generated'; export type ICheckboxContainerProps = PropsFromGenericComponent<'Checkboxes'>; export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxContainerProps) => { const item = useNodeItem(node); - const { id, layout, readOnly, textResourceBindings, required, labelSettings, alertOnChange, showLabelsInTable } = - item; + const { + id, + layout, + readOnly, + textResourceBindings, + required, + labelSettings, + alertOnChange, + showLabelsInTable, + dataModelBindings, + } = item; const { langAsString } = useLanguage(); const { options: calculatedOptions, isFetching, setData, selectedValues } = useGetOptions(node, 'multi'); const isValid = useIsValid(node); + const setMultiLeafValues = FD.useSetMultiLeafValues(); + const labelTextGroup = ( { + const setChecked = (isChecked: boolean, option) => { const newData = isChecked ? [...selectedValues, option.value] : selectedValues.filter((o) => o !== option.value); setData(newData); - - /*if (item.dataModelBindings.saveToList) { - const changes = Object.entries(newData).map((entry) => ({ - reference: { - dataType: item.dataModelBindings.data.dataType, - field: `${item.dataModelBindings.data.field}[${(formData.data as []).length - 1}].${entry[0]}`, - }, - newValue: `${entry[1]}`, - })); - setMultiLeafValues({ changes }); - }*/ }; return ( diff --git a/src/layout/Checkboxes/config.ts b/src/layout/Checkboxes/config.ts index fdb989c2c0..15f7d0bbd0 100644 --- a/src/layout/Checkboxes/config.ts +++ b/src/layout/Checkboxes/config.ts @@ -35,19 +35,6 @@ export const Config = new CG.component({ }) .addPlugin(new OptionsPlugin({ supportsPreselection: true, type: 'multi' })) .addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple')) - .addDataModelBinding( - new CG.obj( - new CG.prop( - 'saveToList', - new CG.dataModelBinding() - .setTitle('SaveToList') - .setDescription( - 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', - ) - .optional(), - ), - ).exportAs('IDataModelBindingsForSaveTolist'), - ) .addProperty(new CG.prop('layout', CG.common('LayoutStyle').optional())) .addProperty( new CG.prop( From 918c889076ce6135b3f5b3987b114dea603a3830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Mon, 3 Mar 2025 13:50:08 +0100 Subject: [PATCH 18/59] WIP support list of objects in checkbox component --- src/codegen/Common.ts | 5 ++++- src/features/saveToList/useSaveToList.ts | 5 +++++ src/layout/Checkboxes/CheckboxesContainerComponent.tsx | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/features/saveToList/useSaveToList.ts diff --git a/src/codegen/Common.ts b/src/codegen/Common.ts index 953c5a6a18..8f435622d7 100644 --- a/src/codegen/Common.ts +++ b/src/codegen/Common.ts @@ -160,6 +160,9 @@ const common = { .setDescription('Describes the location in the data model where the component should store its metadata') .optional(), ), + ), + IDataModelBindigsMultipleSelect: () => + new CG.obj( new CG.prop( 'saveToList', new CG.dataModelBinding() @@ -169,7 +172,7 @@ const common = { ) .optional(), ), - ), + ).extends(CG.common('IDataModelBindingsOptionsSimple')), IDataModelBindingsLikert: () => new CG.obj( new CG.prop( diff --git a/src/features/saveToList/useSaveToList.ts b/src/features/saveToList/useSaveToList.ts new file mode 100644 index 0000000000..986aeee895 --- /dev/null +++ b/src/features/saveToList/useSaveToList.ts @@ -0,0 +1,5 @@ +export const useSaveToList = (node) => { + const setList = () => {}; + + return { setList }; +}; diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx index 32b04c6f37..6642067ab1 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx @@ -58,7 +58,12 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC const setChecked = (isChecked: boolean, option) => { const newData = isChecked ? [...selectedValues, option.value] : selectedValues.filter((o) => o !== option.value); + /*const newList: object[] = []; + if (saveToList) { + setList(newList); + } else {*/ setData(newData); + /*}*/ }; return ( From 6c3cb60b953036209cebf16682afc92c3d1cf8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Tue, 4 Mar 2025 14:32:47 +0100 Subject: [PATCH 19/59] WIP support list of objects in checkbox component --- src/codegen/Common.ts | 12 ---- src/features/options/useGetOptions.ts | 17 +----- src/features/saveToList/useSaveToList.ts | 55 ++++++++++++++++++- .../CheckboxesContainerComponent.tsx | 18 +++--- src/layout/Checkboxes/config.ts | 30 +++++++++- 5 files changed, 94 insertions(+), 38 deletions(-) diff --git a/src/codegen/Common.ts b/src/codegen/Common.ts index 8f435622d7..e3b42295f8 100644 --- a/src/codegen/Common.ts +++ b/src/codegen/Common.ts @@ -161,18 +161,6 @@ const common = { .optional(), ), ), - IDataModelBindigsMultipleSelect: () => - new CG.obj( - new CG.prop( - 'saveToList', - new CG.dataModelBinding() - .setTitle('SaveToList') - .setDescription( - 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', - ) - .optional(), - ), - ).extends(CG.common('IDataModelBindingsOptionsSimple')), IDataModelBindingsLikert: () => new CG.obj( new CG.prop( diff --git a/src/features/options/useGetOptions.ts b/src/features/options/useGetOptions.ts index 0dfee1c062..aa29facdee 100644 --- a/src/features/options/useGetOptions.ts +++ b/src/features/options/useGetOptions.ts @@ -79,9 +79,8 @@ export function useSetOptions( dataModelBindings: IDataModelBindingsOptionsSimple | undefined, options: IOptionInternal[], ): SetOptionsResult { - const { formData, setValue, setValues } = useDataModelBindings(dataModelBindings); + const { formData, setValue } = useDataModelBindings(dataModelBindings); const value = formData.simpleBinding ?? ''; - const saveToList = formData.saveToList; const currentValues = useMemo( () => (value && value.length > 0 ? (valueType === 'multi' ? value.split(',') : [value]) : []), @@ -99,21 +98,9 @@ export function useSetOptions( setValue('simpleBinding', values.at(0)); } else if (valueType === 'multi') { setValue('simpleBinding', values.join(',')); - if (saveToList) { - console.log('Save to list not implemented'); - setValue('saveToList', values); - // const changes = Object.entries(values).map((entry) => ({ - // reference: { - // dataType: formData.saveToList - // field: `${dataModelBindings.data.field}[${(formData.data as []).length - 1}].${entry[0]}`, - // }, - // newValue: `${entry[1]}`, - // })); - // setMultiLeafValues({ changes }); - } } }, - [setValue, valueType, saveToList], + [setValue, valueType], ); return { diff --git a/src/features/saveToList/useSaveToList.ts b/src/features/saveToList/useSaveToList.ts index 986aeee895..ebcdbab27e 100644 --- a/src/features/saveToList/useSaveToList.ts +++ b/src/features/saveToList/useSaveToList.ts @@ -1,5 +1,56 @@ +import { v4 as uuidv4 } from 'uuid'; + +import { FD } from 'src/features/formData/FormDataWrite'; +import { ALTINN_ROW_ID, DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/types'; +import { useDataModelBindings } from 'src/features/formData/useDataModelBindings'; +import { useNodeItem } from 'src/utils/layout/useNodeItem'; +import type { IDataModelBindingsForSaveTolistCheckbox } from 'src/layout/Checkboxes/config.generated'; + +type Row = Record; + export const useSaveToList = (node) => { - const setList = () => {}; + const bindings = useNodeItem(node, (i) => i.dataModelBindings) as IDataModelBindingsForSaveTolistCheckbox; + const { formData } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); + const appendToList = FD.useAppendToList(); + const removeFromList = FD.useRemoveIndexFromList(); + + function isRowChecked(row: Row): boolean { + return (formData?.saveToList as Row[]).some((selectedRow) => + Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]), + ); + } + + const setList = (row) => { + if (!bindings.saveToList) { + return; + } + if (isRowChecked(row)) { + const index = (formData?.saveToList as Row[]).findIndex((selectedRow) => { + const { altinnRowId: _ } = selectedRow; + return Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]); + }); + if (index >= 0) { + removeFromList({ + reference: bindings.saveToList, + index, + }); + } + } else { + const uuid = uuidv4(); + const next: Row = { [ALTINN_ROW_ID]: uuid }; + console.log('bindings', bindings); + for (const binding of Object.keys(bindings)) { + if (binding !== 'saveToList') { + next[binding] = row[binding]; + } + } + console.log('next', next); + appendToList({ + reference: bindings.saveToList, + newValue: { ...next }, + }); + } + }; - return { setList }; + return { setList, saveToList: formData.saveToList }; }; diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx index 6642067ab1..33723e50d2 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx @@ -5,10 +5,10 @@ import cn from 'classnames'; import { AltinnSpinner } from 'src/components/AltinnSpinner'; import { LabelContent } from 'src/components/label/LabelContent'; -import { FD } from 'src/features/formData/FormDataWrite'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import { useGetOptions } from 'src/features/options/useGetOptions'; +import { useSaveToList } from 'src/features/saveToList/useSaveToList'; import { useIsValid } from 'src/features/validation/selectors/isValid'; import classes from 'src/layout/Checkboxes/CheckboxesContainerComponent.module.css'; import { WrappedCheckbox } from 'src/layout/Checkboxes/WrappedCheckbox'; @@ -34,9 +34,9 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC } = item; const { langAsString } = useLanguage(); const { options: calculatedOptions, isFetching, setData, selectedValues } = useGetOptions(node, 'multi'); - const isValid = useIsValid(node); + const { setList } = useSaveToList(node); - const setMultiLeafValues = FD.useSetMultiLeafValues(); + const isValid = useIsValid(node); const labelTextGroup = ( { const newData = isChecked ? [...selectedValues, option.value] : selectedValues.filter((o) => o !== option.value); - /*const newList: object[] = []; - if (saveToList) { - setList(newList); - } else {*/ + //const newList = newData.map((data) => ({ value: data })); + //console.log(newList); + //const newList: object[] = ; + if (dataModelBindings?.saveToList) { + setList({ name: option.value }); + } //else { setData(newData); - /*}*/ + // } }; return ( diff --git a/src/layout/Checkboxes/config.ts b/src/layout/Checkboxes/config.ts index 15f7d0bbd0..fc3ae0e858 100644 --- a/src/layout/Checkboxes/config.ts +++ b/src/layout/Checkboxes/config.ts @@ -34,7 +34,35 @@ export const Config = new CG.component({ }, }) .addPlugin(new OptionsPlugin({ supportsPreselection: true, type: 'multi' })) - .addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple')) + .addDataModelBinding( + new CG.obj( + new CG.prop( + 'saveToList', + new CG.dataModelBinding() + .setTitle('SaveToList') + .setDescription( + 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', + ) + .optional(), + ), + ) + .exportAs('IDataModelBindingsForSaveTolistCheckbox') + .extends(CG.common('IDataModelBindingsOptionsSimple')), + ) + + /*.addDataModelBinding( + new CG.obj( + new CG.prop( + 'saveToList', + new CG.dataModelBinding() + .setTitle('SaveToList') + .setDescription( + 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', + ) + .optional(), + ), + ).extends(CG.common('IDataModelBindingsOptionsSimple')), + )*/ .addProperty(new CG.prop('layout', CG.common('LayoutStyle').optional())) .addProperty( new CG.prop( From 7cc27f9bb96c70c6985414c5b011856a4ffaf510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Wed, 5 Mar 2025 09:07:16 +0100 Subject: [PATCH 20/59] WIP support list of objects in checkbox component --- src/features/saveToList/useSaveToList.ts | 3 +- .../CheckboxesContainerComponent.tsx | 9 ++- src/layout/Checkboxes/index.tsx | 69 ++++++++++++++++++- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/features/saveToList/useSaveToList.ts b/src/features/saveToList/useSaveToList.ts index ebcdbab27e..0c5bc3bb72 100644 --- a/src/features/saveToList/useSaveToList.ts +++ b/src/features/saveToList/useSaveToList.ts @@ -45,6 +45,7 @@ export const useSaveToList = (node) => { } } console.log('next', next); + appendToList({ reference: bindings.saveToList, newValue: { ...next }, @@ -52,5 +53,5 @@ export const useSaveToList = (node) => { } }; - return { setList, saveToList: formData.saveToList }; + return { setList, list: formData.saveToList }; }; diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx index 33723e50d2..aa4674d2d4 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx @@ -34,7 +34,12 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC } = item; const { langAsString } = useLanguage(); const { options: calculatedOptions, isFetching, setData, selectedValues } = useGetOptions(node, 'multi'); - const { setList } = useSaveToList(node); + const { setList, list } = useSaveToList(node); + + console.log(list); + /* const values: string[] = dataModelBindings.saveToList + ? list.map((item) => ({ value: item[dataModelBindings.simpleBinding.field.split('.')[1]] })) + : selectedValues;*/ const isValid = useIsValid(node); @@ -62,8 +67,10 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC //console.log(newList); //const newList: object[] = ; if (dataModelBindings?.saveToList) { + console.log(option); setList({ name: option.value }); } //else { + console.log(newData); setData(newData); // } }; diff --git a/src/layout/Checkboxes/index.tsx b/src/layout/Checkboxes/index.tsx index 0f4a7ff527..4180e892c2 100644 --- a/src/layout/Checkboxes/index.tsx +++ b/src/layout/Checkboxes/index.tsx @@ -59,6 +59,73 @@ export class Checkboxes extends CheckboxesDef { } validateDataModelBindings(ctx: LayoutValidationCtx<'Checkboxes'>): string[] { - return this.validateDataModelBindingsSimple(ctx); + const errors: string[] = []; + const allowedTypes = ['string', 'boolean', 'number', 'integer']; + + const dataModelBindings = ctx.item.dataModelBindings ?? {}; + + if (!dataModelBindings?.saveToList) { + for (const [binding] of Object.entries(dataModelBindings ?? {})) { + const [newErrors] = this.validateDataModelBindingsAny(ctx, binding, allowedTypes, false); + errors.push(...(newErrors || [])); + } + } + + const [newErrors] = this.validateDataModelBindingsAny(ctx, 'saveToList', ['array'], false); + if (newErrors) { + errors.push(...(newErrors || [])); + } + + if (dataModelBindings?.saveToList) { + const saveToListBinding = ctx.lookupBinding(dataModelBindings?.saveToList); + const simpleBinding = ctx.lookupBinding(dataModelBindings?.simpleBinding); + console.log(simpleBinding); + const items = saveToListBinding[0]?.items; + const propertyKey = dataModelBindings.simpleBinding.field.split('.')[1]; + const properties = + items && !Array.isArray(items) && typeof items === 'object' && 'properties' in items + ? items.properties + : undefined; + + if (dataModelBindings.saveToList && items && typeof items === 'object' && 'properties' in items) { + if (properties?.[propertyKey]) { + errors.push(`saveToList must contain a field with the same name as the field simpleBinding`); + } else if ( + typeof properties?.[propertyKey] !== 'object' || + typeof properties?.[propertyKey].type !== 'string' + ) { + errors.push( + `Field ${properties?.[propertyKey]} in saveToList must be one of types ${allowedTypes.join(', ')}`, + ); + } else if (!allowedTypes.includes(properties?.[propertyKey].type)) { + errors.push( + `Field ${properties?.[propertyKey]} in saveToList must be one of types ${allowedTypes.join(', ')}`, + ); + } + } + + /*for (const [binding] of Object.entries(dataModelBindings ?? {})) { + let selectedBinding: JSONSchema7Definition | undefined; + const propertyKey = dataModelBindings.simpleBinding.field.split('.')[1]; + console.log(propertyKey); + if (properties) { + selectedBinding = properties[propertyKey]; + } + console.log(selectedBinding); + if (binding !== 'saveToList' && items && typeof items === 'object' && 'properties' in items) { + if (!selectedBinding) { + errors.push(`saveToList must contain a field with the same name as the field ${binding}`); + } else if (typeof selectedBinding !== 'object' || typeof selectedBinding.type !== 'string') { + errors.push(`Field ${binding} in saveToList must be one of types ${allowedTypes.join(', ')}`); + } else if (!allowedTypes.includes(selectedBinding.type)) { + errors.push(`Field ${binding} in saveToList must be one of types ${allowedTypes.join(', ')}`); + } + } + }*/ + } + + return errors; + + //return this.validateDataModelBindingsSimple(ctx); } } From 68960e431bf313a7524fd3c010dcc115125a9214 Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Wed, 5 Mar 2025 13:42:05 +0100 Subject: [PATCH 21/59] checkbox to repgroup rows wip --- src/features/saveToList/useSaveToList.ts | 21 +++++--- .../CheckboxesContainerComponent.tsx | 11 ++-- src/layout/Checkboxes/index.tsx | 53 +++++-------------- 3 files changed, 34 insertions(+), 51 deletions(-) diff --git a/src/features/saveToList/useSaveToList.ts b/src/features/saveToList/useSaveToList.ts index 0c5bc3bb72..96f12f62e6 100644 --- a/src/features/saveToList/useSaveToList.ts +++ b/src/features/saveToList/useSaveToList.ts @@ -15,9 +15,15 @@ export const useSaveToList = (node) => { const removeFromList = FD.useRemoveIndexFromList(); function isRowChecked(row: Row): boolean { - return (formData?.saveToList as Row[]).some((selectedRow) => - Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]), - ); + console.log('checkboxRow', row); + console.log('checkboxSaveToList', formData?.saveToList); + return (formData?.saveToList as Row[]).some((selectedRow) => { + console.log('selectedRow', selectedRow); + return Object.keys(row).every((key) => { + console.log('row', row); + return Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]; + }); + }); } const setList = (row) => { @@ -38,9 +44,11 @@ export const useSaveToList = (node) => { } else { const uuid = uuidv4(); const next: Row = { [ALTINN_ROW_ID]: uuid }; - console.log('bindings', bindings); for (const binding of Object.keys(bindings)) { - if (binding !== 'saveToList') { + if (binding === 'simpleBinding') { + const propertyName = bindings.simpleBinding.field.split('.')[1]; + next[propertyName] = row[propertyName]; + } else if (binding !== 'saveToList' && binding !== 'simpleBinding') { next[binding] = row[binding]; } } @@ -48,8 +56,9 @@ export const useSaveToList = (node) => { appendToList({ reference: bindings.saveToList, - newValue: { ...next }, + newValue: next, }); + console.log('formData', formData); } }; diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx index aa4674d2d4..3ed678691b 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx @@ -66,13 +66,14 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC //const newList = newData.map((data) => ({ value: data })); //console.log(newList); //const newList: object[] = ; + console.log('newData', newData); if (dataModelBindings?.saveToList) { console.log(option); - setList({ name: option.value }); - } //else { - console.log(newData); - setData(newData); - // } + setList({ [dataModelBindings.simpleBinding.field.split('.')[1]]: option.value }); + } else { + setData(newData); + console.log(newData); + } }; return ( diff --git a/src/layout/Checkboxes/index.tsx b/src/layout/Checkboxes/index.tsx index 4180e892c2..6e5afea1f4 100644 --- a/src/layout/Checkboxes/index.tsx +++ b/src/layout/Checkboxes/index.tsx @@ -77,55 +77,28 @@ export class Checkboxes extends CheckboxesDef { } if (dataModelBindings?.saveToList) { + const isCompatible = dataModelBindings?.simpleBinding?.field.includes(`${dataModelBindings.saveToList.field}.`); + if (!isCompatible) { + errors.push(`simpleBinding must reference a field in saveToList`); + } + const simpleBindingPath = dataModelBindings.simpleBinding?.field.split('.'); const saveToListBinding = ctx.lookupBinding(dataModelBindings?.saveToList); - const simpleBinding = ctx.lookupBinding(dataModelBindings?.simpleBinding); - console.log(simpleBinding); + + // const simpleBinding = ctx.lookupBinding({ + // dataType: dataModelBindings.simpleBinding.dataType, + // field: simpleBindingPath.join('[0].'), + // }); + const items = saveToListBinding[0]?.items; - const propertyKey = dataModelBindings.simpleBinding.field.split('.')[1]; const properties = items && !Array.isArray(items) && typeof items === 'object' && 'properties' in items ? items.properties : undefined; - - if (dataModelBindings.saveToList && items && typeof items === 'object' && 'properties' in items) { - if (properties?.[propertyKey]) { - errors.push(`saveToList must contain a field with the same name as the field simpleBinding`); - } else if ( - typeof properties?.[propertyKey] !== 'object' || - typeof properties?.[propertyKey].type !== 'string' - ) { - errors.push( - `Field ${properties?.[propertyKey]} in saveToList must be one of types ${allowedTypes.join(', ')}`, - ); - } else if (!allowedTypes.includes(properties?.[propertyKey].type)) { - errors.push( - `Field ${properties?.[propertyKey]} in saveToList must be one of types ${allowedTypes.join(', ')}`, - ); - } + if (!(properties && simpleBindingPath[1] in properties)) { + errors.push(`The property ${simpleBindingPath[1]} must be present in saveToList`); } - - /*for (const [binding] of Object.entries(dataModelBindings ?? {})) { - let selectedBinding: JSONSchema7Definition | undefined; - const propertyKey = dataModelBindings.simpleBinding.field.split('.')[1]; - console.log(propertyKey); - if (properties) { - selectedBinding = properties[propertyKey]; - } - console.log(selectedBinding); - if (binding !== 'saveToList' && items && typeof items === 'object' && 'properties' in items) { - if (!selectedBinding) { - errors.push(`saveToList must contain a field with the same name as the field ${binding}`); - } else if (typeof selectedBinding !== 'object' || typeof selectedBinding.type !== 'string') { - errors.push(`Field ${binding} in saveToList must be one of types ${allowedTypes.join(', ')}`); - } else if (!allowedTypes.includes(selectedBinding.type)) { - errors.push(`Field ${binding} in saveToList must be one of types ${allowedTypes.join(', ')}`); - } - } - }*/ } return errors; - - //return this.validateDataModelBindingsSimple(ctx); } } From 0e4b559459f99144a5e7f295d4f86aa19f6d9eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Wed, 5 Mar 2025 14:48:56 +0100 Subject: [PATCH 22/59] =?UTF-8?q?St=C3=B8tte=20for=20saveToList=20i=20Chec?= =?UTF-8?q?kbox=20komponent=20og=20refaktorere=20List=20komponent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/saveToList/useSaveToList.ts | 19 ++------ .../CheckboxesContainerComponent.tsx | 25 ++++------ src/layout/Checkboxes/config.ts | 14 ------ src/layout/Checkboxes/index.tsx | 9 ++-- src/layout/List/ListComponent.tsx | 47 ++----------------- 5 files changed, 22 insertions(+), 92 deletions(-) diff --git a/src/features/saveToList/useSaveToList.ts b/src/features/saveToList/useSaveToList.ts index 96f12f62e6..e7fac5b6de 100644 --- a/src/features/saveToList/useSaveToList.ts +++ b/src/features/saveToList/useSaveToList.ts @@ -15,15 +15,9 @@ export const useSaveToList = (node) => { const removeFromList = FD.useRemoveIndexFromList(); function isRowChecked(row: Row): boolean { - console.log('checkboxRow', row); - console.log('checkboxSaveToList', formData?.saveToList); - return (formData?.saveToList as Row[]).some((selectedRow) => { - console.log('selectedRow', selectedRow); - return Object.keys(row).every((key) => { - console.log('row', row); - return Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]; - }); - }); + return (formData?.saveToList as Row[]).some((selectedRow) => + Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]), + ); } const setList = (row) => { @@ -48,19 +42,16 @@ export const useSaveToList = (node) => { if (binding === 'simpleBinding') { const propertyName = bindings.simpleBinding.field.split('.')[1]; next[propertyName] = row[propertyName]; - } else if (binding !== 'saveToList' && binding !== 'simpleBinding') { + } else if (binding !== 'saveToList' && binding !== 'label' && binding !== 'metadata') { next[binding] = row[binding]; } } - console.log('next', next); - appendToList({ reference: bindings.saveToList, newValue: next, }); - console.log('formData', formData); } }; - return { setList, list: formData.saveToList }; + return { setList, isRowChecked }; }; diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx index 3ed678691b..0b4294cb4f 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx @@ -34,12 +34,8 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC } = item; const { langAsString } = useLanguage(); const { options: calculatedOptions, isFetching, setData, selectedValues } = useGetOptions(node, 'multi'); - const { setList, list } = useSaveToList(node); - - console.log(list); - /* const values: string[] = dataModelBindings.saveToList - ? list.map((item) => ({ value: item[dataModelBindings.simpleBinding.field.split('.')[1]] })) - : selectedValues;*/ + const { setList, isRowChecked } = useSaveToList(node); + const saveToList = dataModelBindings?.saveToList; const isValid = useIsValid(node); @@ -63,16 +59,11 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC const setChecked = (isChecked: boolean, option) => { const newData = isChecked ? [...selectedValues, option.value] : selectedValues.filter((o) => o !== option.value); - //const newList = newData.map((data) => ({ value: data })); - //console.log(newList); - //const newList: object[] = ; - console.log('newData', newData); - if (dataModelBindings?.saveToList) { - console.log(option); + + if (saveToList) { setList({ [dataModelBindings.simpleBinding.field.split('.')[1]]: option.value }); } else { setData(newData); - console.log(newData); } }; @@ -93,7 +84,7 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC hideLegend={overrideDisplay?.renderLegend === false} error={!isValid} aria-label={ariaLabel} - value={selectedValues} + //value={selectedValues} data-testid='checkboxes-fieldset' > {calculatedOptions.map((option) => ( @@ -103,7 +94,11 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC option={option} hideLabel={hideLabel} alertOnChange={alertOnChange} - checked={selectedValues.includes(option.value)} + checked={ + saveToList + ? isRowChecked({ [dataModelBindings.simpleBinding.field.split('.')[1]]: option.value }) + : selectedValues.includes(option.value) + } setChecked={(isChecked) => setChecked(isChecked, option)} /> ))} diff --git a/src/layout/Checkboxes/config.ts b/src/layout/Checkboxes/config.ts index fc3ae0e858..60048ca634 100644 --- a/src/layout/Checkboxes/config.ts +++ b/src/layout/Checkboxes/config.ts @@ -49,20 +49,6 @@ export const Config = new CG.component({ .exportAs('IDataModelBindingsForSaveTolistCheckbox') .extends(CG.common('IDataModelBindingsOptionsSimple')), ) - - /*.addDataModelBinding( - new CG.obj( - new CG.prop( - 'saveToList', - new CG.dataModelBinding() - .setTitle('SaveToList') - .setDescription( - 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', - ) - .optional(), - ), - ).extends(CG.common('IDataModelBindingsOptionsSimple')), - )*/ .addProperty(new CG.prop('layout', CG.common('LayoutStyle').optional())) .addProperty( new CG.prop( diff --git a/src/layout/Checkboxes/index.tsx b/src/layout/Checkboxes/index.tsx index 6e5afea1f4..e73462cf1b 100644 --- a/src/layout/Checkboxes/index.tsx +++ b/src/layout/Checkboxes/index.tsx @@ -78,22 +78,19 @@ export class Checkboxes extends CheckboxesDef { if (dataModelBindings?.saveToList) { const isCompatible = dataModelBindings?.simpleBinding?.field.includes(`${dataModelBindings.saveToList.field}.`); + if (!isCompatible) { errors.push(`simpleBinding must reference a field in saveToList`); } + const simpleBindingPath = dataModelBindings.simpleBinding?.field.split('.'); const saveToListBinding = ctx.lookupBinding(dataModelBindings?.saveToList); - - // const simpleBinding = ctx.lookupBinding({ - // dataType: dataModelBindings.simpleBinding.dataType, - // field: simpleBindingPath.join('[0].'), - // }); - const items = saveToListBinding[0]?.items; const properties = items && !Array.isArray(items) && typeof items === 'object' && 'properties' in items ? items.properties : undefined; + if (!(properties && simpleBindingPath[1] in properties)) { errors.push(`The property ${simpleBindingPath[1]} must be present in saveToList`); } diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index 4463398cae..107ea46e2c 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -3,7 +3,6 @@ import type { AriaAttributes } from 'react'; import { Checkbox, Heading, Radio, Table } from '@digdir/designsystemet-react'; import cn from 'classnames'; -import { v4 as uuidv4 } from 'uuid'; import { Pagination as CustomPagination } from 'src/app-components/Pagination/Pagination'; import { Description } from 'src/components/form/Description'; @@ -11,11 +10,11 @@ import { RadioButton } from 'src/components/form/RadioButton'; import { RequiredIndicator } from 'src/components/form/RequiredIndicator'; import { getLabelId } from 'src/components/label/Label'; import { useDataListQuery } from 'src/features/dataLists/useDataListQuery'; -import { FD } from 'src/features/formData/FormDataWrite'; -import { ALTINN_ROW_ID, DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/types'; +import { DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/types'; import { useDataModelBindings } from 'src/features/formData/useDataModelBindings'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; +import { useSaveToList } from 'src/features/saveToList/useSaveToList'; import { useIsMobile } from 'src/hooks/useDeviceWidths'; import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; import classes from 'src/layout/List/ListComponent.module.css'; @@ -58,11 +57,9 @@ export const ListComponent = ({ node }: IListProps) => { const bindings = item.dataModelBindings ?? ({} as IDataModelBindingsForList); const { formData, setValues } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); + const { setList, isRowChecked } = useSaveToList(node); const saveToList = item.dataModelBindings?.saveToList; - const appendToList = FD.useAppendToList(); - const removeFromList = FD.useRemoveIndexFromList(); - const tableHeadersToShowInMobile = Object.keys(tableHeaders).filter( (key) => !tableHeadersMobile || tableHeadersMobile.includes(key), ); @@ -83,53 +80,17 @@ export const ListComponent = ({ node }: IListProps) => { return JSON.stringify(selectedRow) === JSON.stringify(row); } - function isRowChecked(row: Row): boolean { - return (formData?.saveToList as Row[]).some((selectedRow) => - Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]), - ); - } - const title = item.textResourceBindings?.title; const description = item.textResourceBindings?.description; const handleRowClick = (row: Row) => { if (saveToList) { - handleSelectedCheckboxRow(row); + setList(row); } else { handleSelectedRadioRow({ selectedValue: row }); } }; - const handleSelectedCheckboxRow = (row: Row) => { - if (!saveToList) { - return; - } - if (isRowChecked(row)) { - const index = (formData?.saveToList as Row[]).findIndex((selectedRow) => { - const { altinnRowId: _ } = selectedRow; - return Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]); - }); - if (index >= 0) { - removeFromList({ - reference: saveToList, - index, - }); - } - } else { - const uuid = uuidv4(); - const next: Row = { [ALTINN_ROW_ID]: uuid }; - for (const binding of Object.keys(bindings)) { - if (binding !== 'saveToList') { - next[binding] = row[binding]; - } - } - appendToList({ - reference: saveToList, - newValue: { ...next }, - }); - } - }; - const renderListItems = (row: Row, tableHeaders: { [x: string]: string | undefined }) => tableHeadersToShowInMobile.map((key) => (
From 53c38ab5fc8bac35c640162697a418787c07cf34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Fri, 7 Mar 2025 12:13:08 +0100 Subject: [PATCH 23/59] WIP soft deletion --- src/layout/Checkboxes/config.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/layout/Checkboxes/config.ts b/src/layout/Checkboxes/config.ts index 60048ca634..902561caa0 100644 --- a/src/layout/Checkboxes/config.ts +++ b/src/layout/Checkboxes/config.ts @@ -45,10 +45,20 @@ export const Config = new CG.component({ ) .optional(), ), + new CG.prop( + 'isDeleted', + new CG.dataModelBinding() + .setTitle('IsDeleted') + .setDescription( + 'If deletionStrategy=soft and saveToList is set, this value points to where you want to save deleted status.', + ) + .optional(), + ), ) .exportAs('IDataModelBindingsForSaveTolistCheckbox') .extends(CG.common('IDataModelBindingsOptionsSimple')), ) + .addProperty(new CG.prop('deletionStrategy', new CG.enum('soft', 'hard').optional())) .addProperty(new CG.prop('layout', CG.common('LayoutStyle').optional())) .addProperty( new CG.prop( From 36c04ca988a7880604198d653ac336a9ac03dc65 Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Fri, 7 Mar 2025 13:08:13 +0100 Subject: [PATCH 24/59] add layoutvalidation for savetoList --- src/language/texts/en.ts | 5 +++ src/language/texts/nb.ts | 4 ++ src/language/texts/nn.ts | 4 ++ .../Checkboxes/CheckboxesLayoutValidator.tsx | 42 +++++++++++++++++++ src/layout/Checkboxes/index.tsx | 6 +++ 5 files changed, 61 insertions(+) create mode 100644 src/layout/Checkboxes/CheckboxesLayoutValidator.tsx diff --git a/src/language/texts/en.ts b/src/language/texts/en.ts index 86ee4fc08c..c0664f1767 100644 --- a/src/language/texts/en.ts +++ b/src/language/texts/en.ts @@ -438,6 +438,11 @@ export function en() { "Data type '{0}' is marked as 'disallowUserCreate=true', but the subform component is configured with 'showAddButton=true'. This is a contradiction, as the user will never be permitted to perform the add-button operation.", file_upload_same_binding: 'There are multiple FileUpload components with the same data model binding. Each component must have a unique binding. Other components with the same binding: {0}', + deletion_strategy_no_save_to_list: + 'The fields deletionStrategy and isDeleted can only be used together with saveToList.', + save_to_list_no_deletion_strategy: 'When you have set saveToList, you must also set deletionStrategy.', + soft_delete_no_is_deleted: 'When you have set deletionStrategy to soft, you must also set isDeleted.', + hard_delete_with_is_deleted: 'When you have set deletionStrategy to hard, you cannot set isDeleted.', }, version_error: { version_mismatch: 'Version mismatch', diff --git a/src/language/texts/nb.ts b/src/language/texts/nb.ts index 7f39b18975..6d04931eb0 100644 --- a/src/language/texts/nb.ts +++ b/src/language/texts/nb.ts @@ -439,6 +439,10 @@ export function nb(): FixedLanguageList { "Datatype '{0}' er markert som 'disallowUserCreate=true', men underskjema-komponenten er konfigurert med 'showAddButton=true'. Dette er en motsetning, siden brukeren aldri vil få lov til å utføre handlingene bak legg-til knappen.", file_upload_same_binding: 'Det er flere filopplastingskomponenter med samme datamodell-binding. Hver komponent må ha en unik binding. Andre komponenter med samme binding: {0}', + deletion_strategy_no_save_to_list: 'Feltene deletionStrategy og isDeleted kan kun brukes sammen med saveToList.', + save_to_list_no_deletion_strategy: 'Når du har satt saveToList må du også sette deletionStrategy.', + soft_delete_no_is_deleted: 'Når du har satt deletionStrategy til soft må du også sette isDeleted.', + hard_delete_with_is_deleted: 'Når du har satt deletionStrategy til hard kan du ikke sette isDeleted.', }, version_error: { version_mismatch: 'Versjonsfeil', diff --git a/src/language/texts/nn.ts b/src/language/texts/nn.ts index 21df0fd673..3c2b2f40e6 100644 --- a/src/language/texts/nn.ts +++ b/src/language/texts/nn.ts @@ -439,6 +439,10 @@ export function nn(): FixedLanguageList { "Datatype '{0}' er markert som 'disallowUserCreate=true', men underskjema-komponenten er konfigurert med 'showAddButton=true'. Dette er ei motseiing, Sidan brukaren aldri vil få lov til å utføre handlingane bak legg-til knappen.", file_upload_same_binding: 'Det er fleire filopplastingskomponentar med same datamodellbinding. Kvar komponent må ha ein unik binding. Andre komponentar med same binding: {0}', + deletion_strategy_no_save_to_list: 'Felta deletionStrategy og isDeleted kan berre brukast saman med saveToList.', + save_to_list_no_deletion_strategy: 'Når du har sett saveToList, må du også setje deletionStrategy.', + soft_delete_no_is_deleted: 'Når du har sett deletionStrategy til soft, må du også setje isDeleted.', + hard_delete_with_is_deleted: 'Når du har sett deletionStrategy til hard, kan du ikkje setje isDeleted.', }, version_error: { version_mismatch: 'Versjonsfeil', diff --git a/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx b/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx new file mode 100644 index 0000000000..1c0c2aa8df --- /dev/null +++ b/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx @@ -0,0 +1,42 @@ +import { useEffect } from 'react'; + +import { useLanguage } from 'src/features/language/useLanguage'; +import { NodesInternal } from 'src/utils/layout/NodesContext'; +import type { NodeValidationProps } from 'src/layout/layout'; + +export function CheckboxesLayoutValidator(props: NodeValidationProps<'Checkboxes'>) { + const { node, externalItem } = props; + const { langAsString } = useLanguage(); + const saveToList = externalItem.dataModelBindings.saveToList; + const deletionStrategy = externalItem.deletionStrategy; + const isDeleted = externalItem.dataModelBindings.isDeleted; + + const addError = NodesInternal.useAddError(); + + useEffect(() => { + let error: string | null = null; + + if (!saveToList) { + if (!!deletionStrategy || !!isDeleted) { + error = langAsString('config_error.deletion_strategy_no_save_to_list'); + } + } else if (saveToList) { + if (!deletionStrategy) { + error = langAsString('config_error.save_to_list_no_deletion_strategy'); + } + if (deletionStrategy === 'soft' && !isDeleted) { + error = langAsString('config_error.soft_delete_no_is_deleted'); + } + if (deletionStrategy === 'hard' && !!isDeleted) { + error = langAsString('config_error.hard_delete_with_is_deleted'); + } + } + + if (error) { + addError(error, node); + window.logErrorOnce(`Validation error for '${node.id}': ${error}`); + } + }, [addError, node, deletionStrategy, isDeleted, langAsString, saveToList]); + + return null; +} diff --git a/src/layout/Checkboxes/index.tsx b/src/layout/Checkboxes/index.tsx index e73462cf1b..9f30bca701 100644 --- a/src/layout/Checkboxes/index.tsx +++ b/src/layout/Checkboxes/index.tsx @@ -4,6 +4,7 @@ import type { JSX } from 'react'; import { getCommaSeparatedOptionsToText } from 'src/features/options/getCommaSeparatedOptionsToText'; import { runEmptyFieldValidationOnlySimpleBinding } from 'src/features/validation/nodeValidation/emptyFieldValidation'; import { CheckboxContainerComponent } from 'src/layout/Checkboxes/CheckboxesContainerComponent'; +import { CheckboxesLayoutValidator } from 'src/layout/Checkboxes/CheckboxesLayoutValidator'; import { CheckboxesSummary } from 'src/layout/Checkboxes/CheckboxesSummary'; import { CheckboxesDef } from 'src/layout/Checkboxes/config.def.generated'; import { MultipleChoiceSummary } from 'src/layout/Checkboxes/MultipleChoiceSummary'; @@ -11,6 +12,7 @@ import type { LayoutValidationCtx } from 'src/features/devtools/layoutValidation import type { DisplayDataProps } from 'src/features/displayData'; import type { ComponentValidation, ValidationDataSources } from 'src/features/validation'; import type { PropsFromGenericComponent } from 'src/layout'; +import type { NodeValidationProps } from 'src/layout/layout'; import type { SummaryRendererProps } from 'src/layout/LayoutComponent'; import type { CheckboxSummaryOverrideProps } from 'src/layout/Summary2/config.generated'; import type { Summary2Props } from 'src/layout/Summary2/SummaryComponent2/types'; @@ -58,6 +60,10 @@ export class Checkboxes extends CheckboxesDef { return runEmptyFieldValidationOnlySimpleBinding(node, validationDataSources); } + renderLayoutValidators(props: NodeValidationProps<'Checkboxes'>): JSX.Element | null { + return ; + } + validateDataModelBindings(ctx: LayoutValidationCtx<'Checkboxes'>): string[] { const errors: string[] = []; const allowedTypes = ['string', 'boolean', 'number', 'integer']; From a5c9ddd8800d4e32b28f1218b247bb2f3ebbcb0d Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Fri, 7 Mar 2025 14:13:05 +0100 Subject: [PATCH 25/59] softDelete WIP --- src/features/saveToList/useSaveToList.ts | 26 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/features/saveToList/useSaveToList.ts b/src/features/saveToList/useSaveToList.ts index e7fac5b6de..18e678be78 100644 --- a/src/features/saveToList/useSaveToList.ts +++ b/src/features/saveToList/useSaveToList.ts @@ -10,11 +10,13 @@ type Row = Record; export const useSaveToList = (node) => { const bindings = useNodeItem(node, (i) => i.dataModelBindings) as IDataModelBindingsForSaveTolistCheckbox; - const { formData } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); + const isDeleted = bindings.isDeleted; + const { formData, setValue } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); const appendToList = FD.useAppendToList(); const removeFromList = FD.useRemoveIndexFromList(); function isRowChecked(row: Row): boolean { + console.log('formData?.saveToList', formData?.saveToList); return (formData?.saveToList as Row[]).some((selectedRow) => Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]), ); @@ -29,15 +31,27 @@ export const useSaveToList = (node) => { const { altinnRowId: _ } = selectedRow; return Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]); }); - if (index >= 0) { - removeFromList({ - reference: bindings.saveToList, - index, - }); + if (isDeleted) { + const newSaveToList = [...(formData?.saveToList as Row[])]; + newSaveToList[index] = { + ...newSaveToList[index], + [isDeleted.field.split('.').reverse()[0]]: true, + }; + setValue('saveToList', newSaveToList); + } else { + if (index >= 0) { + removeFromList({ + reference: bindings.saveToList, + index, + }); + } } } else { const uuid = uuidv4(); const next: Row = { [ALTINN_ROW_ID]: uuid }; + if (isDeleted) { + next[isDeleted.field.split('.').reverse()[0]] = false; + } for (const binding of Object.keys(bindings)) { if (binding === 'simpleBinding') { const propertyName = bindings.simpleBinding.field.split('.')[1]; From 7671249939778bbdfb2cbc5f1359e192f8663219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Wed, 19 Mar 2025 10:20:32 +0100 Subject: [PATCH 26/59] WIP soft deletion --- src/features/saveToList/useSaveToList.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/features/saveToList/useSaveToList.ts b/src/features/saveToList/useSaveToList.ts index 18e678be78..6b789195be 100644 --- a/src/features/saveToList/useSaveToList.ts +++ b/src/features/saveToList/useSaveToList.ts @@ -5,18 +5,19 @@ import { ALTINN_ROW_ID, DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/t import { useDataModelBindings } from 'src/features/formData/useDataModelBindings'; import { useNodeItem } from 'src/utils/layout/useNodeItem'; import type { IDataModelBindingsForSaveTolistCheckbox } from 'src/layout/Checkboxes/config.generated'; +import type { IDataModelReference } from 'src/layout/common.generated'; type Row = Record; export const useSaveToList = (node) => { const bindings = useNodeItem(node, (i) => i.dataModelBindings) as IDataModelBindingsForSaveTolistCheckbox; const isDeleted = bindings.isDeleted; - const { formData, setValue } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); + const setLeafValue = FD.useSetLeafValue(); + const { formData } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); const appendToList = FD.useAppendToList(); const removeFromList = FD.useRemoveIndexFromList(); function isRowChecked(row: Row): boolean { - console.log('formData?.saveToList', formData?.saveToList); return (formData?.saveToList as Row[]).some((selectedRow) => Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]), ); @@ -32,12 +33,13 @@ export const useSaveToList = (node) => { return Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]); }); if (isDeleted) { - const newSaveToList = [...(formData?.saveToList as Row[])]; - newSaveToList[index] = { - ...newSaveToList[index], - [isDeleted.field.split('.').reverse()[0]]: true, - }; - setValue('saveToList', newSaveToList); + const pathSegments = bindings.isDeleted?.field.split('.'); + const lastElement = pathSegments?.pop(); + const newPath = `${pathSegments?.join('.')}[${index}].${lastElement}`; + setLeafValue({ + reference: { ...bindings.isDeleted, field: newPath } as IDataModelReference, + newValue: !!isDeleted, + }); } else { if (index >= 0) { removeFromList({ From e49e4820c2f87b6f65394b53f0bac7ddb5362c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Fri, 21 Mar 2025 14:14:45 +0100 Subject: [PATCH 27/59] Add functionality for toggling checkbox and hide row. --- src/features/saveToList/useSaveToList.ts | 68 ++++++++++++++++-------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/src/features/saveToList/useSaveToList.ts b/src/features/saveToList/useSaveToList.ts index 6b789195be..28cceef306 100644 --- a/src/features/saveToList/useSaveToList.ts +++ b/src/features/saveToList/useSaveToList.ts @@ -12,15 +12,30 @@ type Row = Record; export const useSaveToList = (node) => { const bindings = useNodeItem(node, (i) => i.dataModelBindings) as IDataModelBindingsForSaveTolistCheckbox; const isDeleted = bindings.isDeleted; + const isDeletedKey = isDeleted?.field.split('.').pop(); const setLeafValue = FD.useSetLeafValue(); const { formData } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); const appendToList = FD.useAppendToList(); const removeFromList = FD.useRemoveIndexFromList(); - function isRowChecked(row: Row): boolean { - return (formData?.saveToList as Row[]).some((selectedRow) => + const getObjectFromFormDataRow = (row: Row): Row | undefined => + (formData?.saveToList as Row[]).find((selectedRow) => Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]), ); + + const getIndexFromFormDataRow = (row: Row) => + (formData?.saveToList as Row[]).findIndex((selectedRow) => { + const { altinnRowId: _ } = selectedRow; + return Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]); + }); + + function isRowChecked(row: Row): boolean { + const formDataObject = getObjectFromFormDataRow(row); + + if (isDeletedKey) { + return !!formDataObject && !formDataObject[isDeletedKey]; + } + return !!formDataObject; } const setList = (row) => { @@ -28,17 +43,14 @@ export const useSaveToList = (node) => { return; } if (isRowChecked(row)) { - const index = (formData?.saveToList as Row[]).findIndex((selectedRow) => { - const { altinnRowId: _ } = selectedRow; - return Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]); - }); + const index = getIndexFromFormDataRow(row); if (isDeleted) { const pathSegments = bindings.isDeleted?.field.split('.'); const lastElement = pathSegments?.pop(); const newPath = `${pathSegments?.join('.')}[${index}].${lastElement}`; setLeafValue({ reference: { ...bindings.isDeleted, field: newPath } as IDataModelReference, - newValue: !!isDeleted, + newValue: true, }); } else { if (index >= 0) { @@ -49,23 +61,35 @@ export const useSaveToList = (node) => { } } } else { - const uuid = uuidv4(); - const next: Row = { [ALTINN_ROW_ID]: uuid }; - if (isDeleted) { - next[isDeleted.field.split('.').reverse()[0]] = false; - } - for (const binding of Object.keys(bindings)) { - if (binding === 'simpleBinding') { - const propertyName = bindings.simpleBinding.field.split('.')[1]; - next[propertyName] = row[propertyName]; - } else if (binding !== 'saveToList' && binding !== 'label' && binding !== 'metadata') { - next[binding] = row[binding]; + const formDataObject = getObjectFromFormDataRow(row); + if (formDataObject && isDeletedKey && formDataObject[isDeletedKey]) { + const index = getIndexFromFormDataRow(row); + const pathSegments = bindings.isDeleted?.field.split('.'); + const lastElement = pathSegments?.pop(); + const newPath = `${pathSegments?.join('.')}[${index}].${lastElement}`; + setLeafValue({ + reference: { ...bindings.isDeleted, field: newPath } as IDataModelReference, + newValue: false, + }); + } else { + const uuid = uuidv4(); + const next: Row = { [ALTINN_ROW_ID]: uuid }; + if (isDeletedKey) { + next[isDeletedKey] = false; } + for (const binding of Object.keys(bindings)) { + if (binding === 'simpleBinding') { + const propertyName = bindings.simpleBinding.field.split('.')[1]; + next[propertyName] = row[propertyName]; + } else if (binding !== 'saveToList' && binding !== 'label' && binding !== 'metadata') { + next[binding] = row[binding]; + } + } + appendToList({ + reference: bindings.saveToList, + newValue: next, + }); } - appendToList({ - reference: bindings.saveToList, - newValue: next, - }); } }; From ade8324eff7366e33e299ae53d528caf7f8074f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Wed, 26 Mar 2025 09:35:36 +0100 Subject: [PATCH 28/59] Refaktorering og kode rydding --- src/features/saveToList/useSaveToList.ts | 36 ++++++++++--------- .../CheckboxesContainerComponent.tsx | 30 ++++++++-------- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/features/saveToList/useSaveToList.ts b/src/features/saveToList/useSaveToList.ts index 71d1065518..27a84965e1 100644 --- a/src/features/saveToList/useSaveToList.ts +++ b/src/features/saveToList/useSaveToList.ts @@ -6,13 +6,16 @@ import { useDataModelBindings } from 'src/features/formData/useDataModelBindings import { useNodeItem } from 'src/utils/layout/useNodeItem'; import type { IDataModelBindingsForSaveTolistCheckbox } from 'src/layout/Checkboxes/config.generated'; import type { IDataModelReference } from 'src/layout/common.generated'; +import type { LayoutNode } from 'src/utils/layout/LayoutNode'; type Row = Record; -export const useSaveToList = (node) => { +export const useSaveToList = (node: LayoutNode<'List' | 'Checkboxes' | 'MultipleSelect'>) => { const bindings = useNodeItem(node, (i) => i.dataModelBindings) as IDataModelBindingsForSaveTolistCheckbox; const isDeleted = bindings.isDeleted; - const isDeletedKey = isDeleted?.field.split('.').pop(); + const isDeletedSegments = isDeleted?.field.split('.'); + const isDeletedKey = isDeletedSegments?.pop(); + const setLeafValue = FD.useSetLeafValue(); const { formData } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); const appendToList = FD.useAppendToList(); @@ -29,27 +32,26 @@ export const useSaveToList = (node) => { return Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]); }); - function isRowChecked(row: Row): boolean { + const isRowChecked = (row: Row): boolean => { const formDataObject = getObjectFromFormDataRow(row); - if (isDeletedKey) { return !!formDataObject && !formDataObject[isDeletedKey]; } return !!formDataObject; - } + }; - const setList = (row) => { + const setList = (row: Row) => { if (!bindings.saveToList) { return; } if (isRowChecked(row)) { const index = getIndexFromFormDataRow(row); if (isDeleted) { - const pathSegments = bindings.isDeleted?.field.split('.'); - const lastElement = pathSegments?.pop(); - const newPath = `${pathSegments?.join('.')}[${index}].${lastElement}`; setLeafValue({ - reference: { ...bindings.isDeleted, field: newPath } as IDataModelReference, + reference: { + ...bindings.isDeleted, + field: `${isDeletedSegments?.join('.')}[${index}].${isDeletedKey}`, + } as IDataModelReference, newValue: true, }); } else { @@ -64,11 +66,11 @@ export const useSaveToList = (node) => { const formDataObject = getObjectFromFormDataRow(row); if (formDataObject && isDeletedKey && formDataObject[isDeletedKey]) { const index = getIndexFromFormDataRow(row); - const pathSegments = bindings.isDeleted?.field.split('.'); - const lastElement = pathSegments?.pop(); - const newPath = `${pathSegments?.join('.')}[${index}].${lastElement}`; setLeafValue({ - reference: { ...bindings.isDeleted, field: newPath } as IDataModelReference, + reference: { + ...bindings.isDeleted, + field: `${isDeletedSegments?.join('.')}[${index}].${isDeletedKey}`, + } as IDataModelReference, newValue: false, }); } else { @@ -79,8 +81,10 @@ export const useSaveToList = (node) => { } for (const binding of Object.keys(bindings)) { if (binding === 'simpleBinding') { - const propertyName = bindings.simpleBinding.field.split('.')[1]; - next[propertyName] = row[propertyName]; + const propertyName = bindings.simpleBinding.field.split('.').pop(); + if (propertyName) { + next[propertyName] = row[propertyName]; + } } else if (binding !== 'saveToList' && binding !== 'label' && binding !== 'metadata') { next[binding] = row[binding]; } diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx index 0b4294cb4f..bac460cd04 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx @@ -38,24 +38,14 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC const saveToList = dataModelBindings?.saveToList; const isValid = useIsValid(node); - - const labelTextGroup = ( - - ); - const horizontal = shouldUseRowLayout({ layout, optionsCount: calculatedOptions.length, }); + const hideLabel = overrideDisplay?.renderedInTable === true && calculatedOptions.length === 1 && !showLabelsInTable; const ariaLabel = overrideDisplay?.renderedInTable ? langAsString(textResourceBindings?.title) : undefined; + const rowKey = dataModelBindings.simpleBinding.field.split('.').pop(); const setChecked = (isChecked: boolean, option) => { const newData = isChecked ? [...selectedValues, option.value] : selectedValues.filter((o) => o !== option.value); @@ -67,6 +57,17 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC } }; + const labelTextGroup = ( + + ); + return ( {isFetching ? ( @@ -84,7 +85,6 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC hideLegend={overrideDisplay?.renderLegend === false} error={!isValid} aria-label={ariaLabel} - //value={selectedValues} data-testid='checkboxes-fieldset' > {calculatedOptions.map((option) => ( @@ -95,8 +95,8 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC hideLabel={hideLabel} alertOnChange={alertOnChange} checked={ - saveToList - ? isRowChecked({ [dataModelBindings.simpleBinding.field.split('.')[1]]: option.value }) + saveToList && rowKey + ? isRowChecked({ [rowKey]: option.value }) : selectedValues.includes(option.value) } setChecked={(isChecked) => setChecked(isChecked, option)} From 1d9c83969f654d5667c11c30aec87b57b12cb770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Thu, 27 Mar 2025 14:28:41 +0100 Subject: [PATCH 29/59] Rename to group from saveToList --- ...seSaveToList.ts => useSaveObjectToGroup.ts} | 18 +++++++++--------- src/language/texts/en.ts | 4 ++-- src/language/texts/nb.ts | 4 ++-- src/language/texts/nn.ts | 4 ++-- .../CheckboxesContainerComponent.tsx | 12 +++++------- .../Checkboxes/CheckboxesLayoutValidator.tsx | 8 ++++---- src/layout/Checkboxes/config.ts | 8 ++++---- src/layout/Checkboxes/index.tsx | 8 ++++---- 8 files changed, 32 insertions(+), 34 deletions(-) rename src/features/saveToList/{useSaveToList.ts => useSaveObjectToGroup.ts} (84%) diff --git a/src/features/saveToList/useSaveToList.ts b/src/features/saveToList/useSaveObjectToGroup.ts similarity index 84% rename from src/features/saveToList/useSaveToList.ts rename to src/features/saveToList/useSaveObjectToGroup.ts index 27a84965e1..19ce575cde 100644 --- a/src/features/saveToList/useSaveToList.ts +++ b/src/features/saveToList/useSaveObjectToGroup.ts @@ -4,14 +4,14 @@ import { FD } from 'src/features/formData/FormDataWrite'; import { ALTINN_ROW_ID, DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/types'; import { useDataModelBindings } from 'src/features/formData/useDataModelBindings'; import { useNodeItem } from 'src/utils/layout/useNodeItem'; -import type { IDataModelBindingsForSaveTolistCheckbox } from 'src/layout/Checkboxes/config.generated'; +import type { IDataModelBindingsForGroupCheckbox } from 'src/layout/Checkboxes/config.generated'; import type { IDataModelReference } from 'src/layout/common.generated'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; type Row = Record; -export const useSaveToList = (node: LayoutNode<'List' | 'Checkboxes' | 'MultipleSelect'>) => { - const bindings = useNodeItem(node, (i) => i.dataModelBindings) as IDataModelBindingsForSaveTolistCheckbox; +export const useSaveObjectToGroup = (node: LayoutNode<'List' | 'Checkboxes' | 'MultipleSelect'>) => { + const bindings = useNodeItem(node, (i) => i.dataModelBindings) as IDataModelBindingsForGroupCheckbox; const isDeleted = bindings.isDeleted; const isDeletedSegments = isDeleted?.field.split('.'); const isDeletedKey = isDeletedSegments?.pop(); @@ -22,12 +22,12 @@ export const useSaveToList = (node: LayoutNode<'List' | 'Checkboxes' | 'Multiple const removeFromList = FD.useRemoveIndexFromList(); const getObjectFromFormDataRow = (row: Row): Row | undefined => - (formData?.saveToList as Row[])?.find((selectedRow) => + (formData?.group as Row[])?.find((selectedRow) => Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]), ); const getIndexFromFormDataRow = (row: Row) => - (formData?.saveToList as Row[]).findIndex((selectedRow) => { + (formData?.group as Row[]).findIndex((selectedRow) => { const { altinnRowId: _ } = selectedRow; return Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]); }); @@ -41,7 +41,7 @@ export const useSaveToList = (node: LayoutNode<'List' | 'Checkboxes' | 'Multiple }; const setList = (row: Row) => { - if (!bindings.saveToList) { + if (!bindings.group) { return; } if (isRowChecked(row)) { @@ -57,7 +57,7 @@ export const useSaveToList = (node: LayoutNode<'List' | 'Checkboxes' | 'Multiple } else { if (index >= 0) { removeFromList({ - reference: bindings.saveToList, + reference: bindings.group, index, }); } @@ -85,12 +85,12 @@ export const useSaveToList = (node: LayoutNode<'List' | 'Checkboxes' | 'Multiple if (propertyName) { next[propertyName] = row[propertyName]; } - } else if (binding !== 'saveToList' && binding !== 'label' && binding !== 'metadata') { + } else if (binding !== 'group' && binding !== 'label' && binding !== 'metadata') { next[binding] = row[binding]; } } appendToList({ - reference: bindings.saveToList, + reference: bindings.group, newValue: next, }); } diff --git a/src/language/texts/en.ts b/src/language/texts/en.ts index c0664f1767..5ac0632f2c 100644 --- a/src/language/texts/en.ts +++ b/src/language/texts/en.ts @@ -439,8 +439,8 @@ export function en() { file_upload_same_binding: 'There are multiple FileUpload components with the same data model binding. Each component must have a unique binding. Other components with the same binding: {0}', deletion_strategy_no_save_to_list: - 'The fields deletionStrategy and isDeleted can only be used together with saveToList.', - save_to_list_no_deletion_strategy: 'When you have set saveToList, you must also set deletionStrategy.', + 'The fields deletionStrategy and isDeleted can only be used together with group.', + save_to_list_no_deletion_strategy: 'When you have set group, you must also set deletionStrategy.', soft_delete_no_is_deleted: 'When you have set deletionStrategy to soft, you must also set isDeleted.', hard_delete_with_is_deleted: 'When you have set deletionStrategy to hard, you cannot set isDeleted.', }, diff --git a/src/language/texts/nb.ts b/src/language/texts/nb.ts index 6d04931eb0..5b7f545c76 100644 --- a/src/language/texts/nb.ts +++ b/src/language/texts/nb.ts @@ -439,8 +439,8 @@ export function nb(): FixedLanguageList { "Datatype '{0}' er markert som 'disallowUserCreate=true', men underskjema-komponenten er konfigurert med 'showAddButton=true'. Dette er en motsetning, siden brukeren aldri vil få lov til å utføre handlingene bak legg-til knappen.", file_upload_same_binding: 'Det er flere filopplastingskomponenter med samme datamodell-binding. Hver komponent må ha en unik binding. Andre komponenter med samme binding: {0}', - deletion_strategy_no_save_to_list: 'Feltene deletionStrategy og isDeleted kan kun brukes sammen med saveToList.', - save_to_list_no_deletion_strategy: 'Når du har satt saveToList må du også sette deletionStrategy.', + deletion_strategy_no_save_to_list: 'Feltene deletionStrategy og isDeleted kan kun brukes sammen med group.', + save_to_list_no_deletion_strategy: 'Når du har satt group må du også sette deletionStrategy.', soft_delete_no_is_deleted: 'Når du har satt deletionStrategy til soft må du også sette isDeleted.', hard_delete_with_is_deleted: 'Når du har satt deletionStrategy til hard kan du ikke sette isDeleted.', }, diff --git a/src/language/texts/nn.ts b/src/language/texts/nn.ts index 3c2b2f40e6..6f05830d79 100644 --- a/src/language/texts/nn.ts +++ b/src/language/texts/nn.ts @@ -439,8 +439,8 @@ export function nn(): FixedLanguageList { "Datatype '{0}' er markert som 'disallowUserCreate=true', men underskjema-komponenten er konfigurert med 'showAddButton=true'. Dette er ei motseiing, Sidan brukaren aldri vil få lov til å utføre handlingane bak legg-til knappen.", file_upload_same_binding: 'Det er fleire filopplastingskomponentar med same datamodellbinding. Kvar komponent må ha ein unik binding. Andre komponentar med same binding: {0}', - deletion_strategy_no_save_to_list: 'Felta deletionStrategy og isDeleted kan berre brukast saman med saveToList.', - save_to_list_no_deletion_strategy: 'Når du har sett saveToList, må du også setje deletionStrategy.', + deletion_strategy_no_save_to_list: 'Felta deletionStrategy og isDeleted kan berre brukast saman med group.', + save_to_list_no_deletion_strategy: 'Når du har sett group, må du også setje deletionStrategy.', soft_delete_no_is_deleted: 'Når du har sett deletionStrategy til soft, må du også setje isDeleted.', hard_delete_with_is_deleted: 'Når du har sett deletionStrategy til hard, kan du ikkje setje isDeleted.', }, diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx index bac460cd04..5473fc6876 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx @@ -8,7 +8,7 @@ import { LabelContent } from 'src/components/label/LabelContent'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import { useGetOptions } from 'src/features/options/useGetOptions'; -import { useSaveToList } from 'src/features/saveToList/useSaveToList'; +import { useSaveObjectToGroup } from 'src/features/saveToList/useSaveObjectToGroup'; import { useIsValid } from 'src/features/validation/selectors/isValid'; import classes from 'src/layout/Checkboxes/CheckboxesContainerComponent.module.css'; import { WrappedCheckbox } from 'src/layout/Checkboxes/WrappedCheckbox'; @@ -34,8 +34,8 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC } = item; const { langAsString } = useLanguage(); const { options: calculatedOptions, isFetching, setData, selectedValues } = useGetOptions(node, 'multi'); - const { setList, isRowChecked } = useSaveToList(node); - const saveToList = dataModelBindings?.saveToList; + const { setList, isRowChecked } = useSaveObjectToGroup(node); + const group = dataModelBindings?.group; const isValid = useIsValid(node); const horizontal = shouldUseRowLayout({ @@ -50,7 +50,7 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC const setChecked = (isChecked: boolean, option) => { const newData = isChecked ? [...selectedValues, option.value] : selectedValues.filter((o) => o !== option.value); - if (saveToList) { + if (group) { setList({ [dataModelBindings.simpleBinding.field.split('.')[1]]: option.value }); } else { setData(newData); @@ -95,9 +95,7 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC hideLabel={hideLabel} alertOnChange={alertOnChange} checked={ - saveToList && rowKey - ? isRowChecked({ [rowKey]: option.value }) - : selectedValues.includes(option.value) + group && rowKey ? isRowChecked({ [rowKey]: option.value }) : selectedValues.includes(option.value) } setChecked={(isChecked) => setChecked(isChecked, option)} /> diff --git a/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx b/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx index 1c0c2aa8df..4a8b0ec97f 100644 --- a/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx +++ b/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx @@ -7,7 +7,7 @@ import type { NodeValidationProps } from 'src/layout/layout'; export function CheckboxesLayoutValidator(props: NodeValidationProps<'Checkboxes'>) { const { node, externalItem } = props; const { langAsString } = useLanguage(); - const saveToList = externalItem.dataModelBindings.saveToList; + const group = externalItem.dataModelBindings.group; const deletionStrategy = externalItem.deletionStrategy; const isDeleted = externalItem.dataModelBindings.isDeleted; @@ -16,11 +16,11 @@ export function CheckboxesLayoutValidator(props: NodeValidationProps<'Checkboxes useEffect(() => { let error: string | null = null; - if (!saveToList) { + if (!group) { if (!!deletionStrategy || !!isDeleted) { error = langAsString('config_error.deletion_strategy_no_save_to_list'); } - } else if (saveToList) { + } else if (group) { if (!deletionStrategy) { error = langAsString('config_error.save_to_list_no_deletion_strategy'); } @@ -36,7 +36,7 @@ export function CheckboxesLayoutValidator(props: NodeValidationProps<'Checkboxes addError(error, node); window.logErrorOnce(`Validation error for '${node.id}': ${error}`); } - }, [addError, node, deletionStrategy, isDeleted, langAsString, saveToList]); + }, [addError, node, deletionStrategy, isDeleted, langAsString, group]); return null; } diff --git a/src/layout/Checkboxes/config.ts b/src/layout/Checkboxes/config.ts index 902561caa0..02e2db15dd 100644 --- a/src/layout/Checkboxes/config.ts +++ b/src/layout/Checkboxes/config.ts @@ -37,9 +37,9 @@ export const Config = new CG.component({ .addDataModelBinding( new CG.obj( new CG.prop( - 'saveToList', + 'group', new CG.dataModelBinding() - .setTitle('SaveToList') + .setTitle('group') .setDescription( 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', ) @@ -50,12 +50,12 @@ export const Config = new CG.component({ new CG.dataModelBinding() .setTitle('IsDeleted') .setDescription( - 'If deletionStrategy=soft and saveToList is set, this value points to where you want to save deleted status.', + 'If deletionStrategy=soft and group is set, this value points to where you want to save deleted status.', ) .optional(), ), ) - .exportAs('IDataModelBindingsForSaveTolistCheckbox') + .exportAs('IDataModelBindingsForGroupCheckbox') .extends(CG.common('IDataModelBindingsOptionsSimple')), ) .addProperty(new CG.prop('deletionStrategy', new CG.enum('soft', 'hard').optional())) diff --git a/src/layout/Checkboxes/index.tsx b/src/layout/Checkboxes/index.tsx index 9ab14895aa..80c7817b62 100644 --- a/src/layout/Checkboxes/index.tsx +++ b/src/layout/Checkboxes/index.tsx @@ -64,7 +64,7 @@ export class Checkboxes extends CheckboxesDef { const dataModelBindings = ctx.item.dataModelBindings ?? {}; - if (!dataModelBindings?.saveToList) { + if (!dataModelBindings?.group) { for (const [binding] of Object.entries(dataModelBindings ?? {})) { const [newErrors] = this.validateDataModelBindingsAny(ctx, binding, allowedTypes, false); errors.push(...(newErrors || [])); @@ -76,15 +76,15 @@ export class Checkboxes extends CheckboxesDef { errors.push(...(newErrors || [])); } - if (dataModelBindings?.saveToList) { - const isCompatible = dataModelBindings?.simpleBinding?.field.includes(`${dataModelBindings.saveToList.field}.`); + if (dataModelBindings?.group) { + const isCompatible = dataModelBindings?.simpleBinding?.field.includes(`${dataModelBindings.group.field}.`); if (!isCompatible) { errors.push(`simpleBinding must reference a field in saveToList`); } const simpleBindingPath = dataModelBindings.simpleBinding?.field.split('.'); - const saveToListBinding = ctx.lookupBinding(dataModelBindings?.saveToList); + const saveToListBinding = ctx.lookupBinding(dataModelBindings?.group); const items = saveToListBinding[0]?.items; const properties = items && !Array.isArray(items) && typeof items === 'object' && 'properties' in items From 622ecdbd5a39d751b27af81e41452ed86c505ca3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Tue, 1 Apr 2025 11:18:52 +0200 Subject: [PATCH 30/59] Rename to checked from isDeleted --- .../saveToList/useSaveObjectToGroup.ts | 30 ++++++++--------- src/language/texts/en.ts | 6 ++-- src/language/texts/nb.ts | 6 ++-- src/language/texts/nn.ts | 6 ++-- .../Checkboxes/CheckboxesLayoutValidator.tsx | 10 +++--- src/layout/Checkboxes/config.ts | 4 +-- src/layout/List/ListComponent.tsx | 33 ++++++++++--------- 7 files changed, 48 insertions(+), 47 deletions(-) diff --git a/src/features/saveToList/useSaveObjectToGroup.ts b/src/features/saveToList/useSaveObjectToGroup.ts index 19ce575cde..82c93f7fa8 100644 --- a/src/features/saveToList/useSaveObjectToGroup.ts +++ b/src/features/saveToList/useSaveObjectToGroup.ts @@ -12,9 +12,9 @@ type Row = Record; export const useSaveObjectToGroup = (node: LayoutNode<'List' | 'Checkboxes' | 'MultipleSelect'>) => { const bindings = useNodeItem(node, (i) => i.dataModelBindings) as IDataModelBindingsForGroupCheckbox; - const isDeleted = bindings.isDeleted; - const isDeletedSegments = isDeleted?.field.split('.'); - const isDeletedKey = isDeletedSegments?.pop(); + const checkedBinding = bindings.checked; + const checkedBindingSegments = checkedBinding?.field.split('.'); + const checkedBindingKey = checkedBindingSegments?.pop(); const setLeafValue = FD.useSetLeafValue(); const { formData } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); @@ -34,8 +34,8 @@ export const useSaveObjectToGroup = (node: LayoutNode<'List' | 'Checkboxes' | 'M const isRowChecked = (row: Row): boolean => { const formDataObject = getObjectFromFormDataRow(row); - if (isDeletedKey) { - return !!formDataObject && !formDataObject[isDeletedKey]; + if (checkedBindingKey) { + return !!formDataObject && !!formDataObject[checkedBindingKey]; } return !!formDataObject; }; @@ -46,13 +46,13 @@ export const useSaveObjectToGroup = (node: LayoutNode<'List' | 'Checkboxes' | 'M } if (isRowChecked(row)) { const index = getIndexFromFormDataRow(row); - if (isDeleted) { + if (checkedBinding) { setLeafValue({ reference: { - ...bindings.isDeleted, - field: `${isDeletedSegments?.join('.')}[${index}].${isDeletedKey}`, + ...bindings.checked, + field: `${checkedBindingSegments?.join('.')}[${index}].${checkedBindingKey}`, } as IDataModelReference, - newValue: true, + newValue: false, }); } else { if (index >= 0) { @@ -64,20 +64,20 @@ export const useSaveObjectToGroup = (node: LayoutNode<'List' | 'Checkboxes' | 'M } } else { const formDataObject = getObjectFromFormDataRow(row); - if (formDataObject && isDeletedKey && formDataObject[isDeletedKey]) { + if (formDataObject && checkedBindingKey && formDataObject[checkedBindingKey]) { const index = getIndexFromFormDataRow(row); setLeafValue({ reference: { - ...bindings.isDeleted, - field: `${isDeletedSegments?.join('.')}[${index}].${isDeletedKey}`, + ...bindings.checked, + field: `${checkedBindingSegments?.join('.')}[${index}].${checkedBindingKey}`, } as IDataModelReference, - newValue: false, + newValue: true, }); } else { const uuid = uuidv4(); const next: Row = { [ALTINN_ROW_ID]: uuid }; - if (isDeletedKey) { - next[isDeletedKey] = false; + if (checkedBindingKey) { + next[checkedBindingKey] = true; } for (const binding of Object.keys(bindings)) { if (binding === 'simpleBinding') { diff --git a/src/language/texts/en.ts b/src/language/texts/en.ts index 5ac0632f2c..bb7d73b050 100644 --- a/src/language/texts/en.ts +++ b/src/language/texts/en.ts @@ -439,10 +439,10 @@ export function en() { file_upload_same_binding: 'There are multiple FileUpload components with the same data model binding. Each component must have a unique binding. Other components with the same binding: {0}', deletion_strategy_no_save_to_list: - 'The fields deletionStrategy and isDeleted can only be used together with group.', + 'The fields deletionStrategy and checked can only be used together with group.', save_to_list_no_deletion_strategy: 'When you have set group, you must also set deletionStrategy.', - soft_delete_no_is_deleted: 'When you have set deletionStrategy to soft, you must also set isDeleted.', - hard_delete_with_is_deleted: 'When you have set deletionStrategy to hard, you cannot set isDeleted.', + soft_delete_no_is_deleted: 'When you have set deletionStrategy to soft, you must also set "checked".', + hard_delete_with_is_deleted: 'When you have set deletionStrategy to hard, you cannot set "checked".', }, version_error: { version_mismatch: 'Version mismatch', diff --git a/src/language/texts/nb.ts b/src/language/texts/nb.ts index 5b7f545c76..d1f11f80fb 100644 --- a/src/language/texts/nb.ts +++ b/src/language/texts/nb.ts @@ -439,10 +439,10 @@ export function nb(): FixedLanguageList { "Datatype '{0}' er markert som 'disallowUserCreate=true', men underskjema-komponenten er konfigurert med 'showAddButton=true'. Dette er en motsetning, siden brukeren aldri vil få lov til å utføre handlingene bak legg-til knappen.", file_upload_same_binding: 'Det er flere filopplastingskomponenter med samme datamodell-binding. Hver komponent må ha en unik binding. Andre komponenter med samme binding: {0}', - deletion_strategy_no_save_to_list: 'Feltene deletionStrategy og isDeleted kan kun brukes sammen med group.', + deletion_strategy_no_save_to_list: 'Feltene deletionStrategy og checked kan kun brukes sammen med group.', save_to_list_no_deletion_strategy: 'Når du har satt group må du også sette deletionStrategy.', - soft_delete_no_is_deleted: 'Når du har satt deletionStrategy til soft må du også sette isDeleted.', - hard_delete_with_is_deleted: 'Når du har satt deletionStrategy til hard kan du ikke sette isDeleted.', + soft_delete_no_is_deleted: 'Når du har satt deletionStrategy til soft må du også sette "checked".', + hard_delete_with_is_deleted: 'Når du har satt deletionStrategy til hard kan du ikke sette "checked".', }, version_error: { version_mismatch: 'Versjonsfeil', diff --git a/src/language/texts/nn.ts b/src/language/texts/nn.ts index 6f05830d79..3e8436b738 100644 --- a/src/language/texts/nn.ts +++ b/src/language/texts/nn.ts @@ -439,10 +439,10 @@ export function nn(): FixedLanguageList { "Datatype '{0}' er markert som 'disallowUserCreate=true', men underskjema-komponenten er konfigurert med 'showAddButton=true'. Dette er ei motseiing, Sidan brukaren aldri vil få lov til å utføre handlingane bak legg-til knappen.", file_upload_same_binding: 'Det er fleire filopplastingskomponentar med same datamodellbinding. Kvar komponent må ha ein unik binding. Andre komponentar med same binding: {0}', - deletion_strategy_no_save_to_list: 'Felta deletionStrategy og isDeleted kan berre brukast saman med group.', + deletion_strategy_no_save_to_list: 'Felta deletionStrategy og checked kan berre brukast saman med group.', save_to_list_no_deletion_strategy: 'Når du har sett group, må du også setje deletionStrategy.', - soft_delete_no_is_deleted: 'Når du har sett deletionStrategy til soft, må du også setje isDeleted.', - hard_delete_with_is_deleted: 'Når du har sett deletionStrategy til hard, kan du ikkje setje isDeleted.', + soft_delete_no_is_deleted: 'Når du har sett deletionStrategy til soft, må du også setje checked.', + hard_delete_with_is_deleted: 'Når du har sett deletionStrategy til hard, kan du ikkje setje checked.', }, version_error: { version_mismatch: 'Versjonsfeil', diff --git a/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx b/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx index 4a8b0ec97f..dc985f2c41 100644 --- a/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx +++ b/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx @@ -9,7 +9,7 @@ export function CheckboxesLayoutValidator(props: NodeValidationProps<'Checkboxes const { langAsString } = useLanguage(); const group = externalItem.dataModelBindings.group; const deletionStrategy = externalItem.deletionStrategy; - const isDeleted = externalItem.dataModelBindings.isDeleted; + const checkedBinding = externalItem.dataModelBindings.checked; const addError = NodesInternal.useAddError(); @@ -17,17 +17,17 @@ export function CheckboxesLayoutValidator(props: NodeValidationProps<'Checkboxes let error: string | null = null; if (!group) { - if (!!deletionStrategy || !!isDeleted) { + if (!!deletionStrategy || !!checkedBinding) { error = langAsString('config_error.deletion_strategy_no_save_to_list'); } } else if (group) { if (!deletionStrategy) { error = langAsString('config_error.save_to_list_no_deletion_strategy'); } - if (deletionStrategy === 'soft' && !isDeleted) { + if (deletionStrategy === 'soft' && !checkedBinding) { error = langAsString('config_error.soft_delete_no_is_deleted'); } - if (deletionStrategy === 'hard' && !!isDeleted) { + if (deletionStrategy === 'hard' && !!checkedBinding) { error = langAsString('config_error.hard_delete_with_is_deleted'); } } @@ -36,7 +36,7 @@ export function CheckboxesLayoutValidator(props: NodeValidationProps<'Checkboxes addError(error, node); window.logErrorOnce(`Validation error for '${node.id}': ${error}`); } - }, [addError, node, deletionStrategy, isDeleted, langAsString, group]); + }, [addError, node, deletionStrategy, checkedBinding, langAsString, group]); return null; } diff --git a/src/layout/Checkboxes/config.ts b/src/layout/Checkboxes/config.ts index 02e2db15dd..c89644f6db 100644 --- a/src/layout/Checkboxes/config.ts +++ b/src/layout/Checkboxes/config.ts @@ -46,9 +46,9 @@ export const Config = new CG.component({ .optional(), ), new CG.prop( - 'isDeleted', + 'checked', new CG.dataModelBinding() - .setTitle('IsDeleted') + .setTitle('checked') .setDescription( 'If deletionStrategy=soft and group is set, this value points to where you want to save deleted status.', ) diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index d6549052a4..c2c7934114 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -3,7 +3,6 @@ import type { AriaAttributes } from 'react'; import { Checkbox, Heading, Radio, Table } from '@digdir/designsystemet-react'; import cn from 'classnames'; -import { v4 as uuidv4 } from 'uuid'; import { Pagination as CustomPagination } from 'src/app-components/Pagination/Pagination'; import { Description } from 'src/components/form/Description'; @@ -11,11 +10,11 @@ import { RadioButton } from 'src/components/form/RadioButton'; import { RequiredIndicator } from 'src/components/form/RequiredIndicator'; import { getLabelId } from 'src/components/label/Label'; import { useDataListQuery } from 'src/features/dataLists/useDataListQuery'; -import { FD } from 'src/features/formData/FormDataWrite'; -import { ALTINN_ROW_ID, DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/types'; +import { DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/types'; import { useDataModelBindings } from 'src/features/formData/useDataModelBindings'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; +import { useSaveObjectToGroup } from 'src/features/saveToList/useSaveObjectToGroup'; import { useIsMobile } from 'src/hooks/useDeviceWidths'; import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; import classes from 'src/layout/List/ListComponent.module.css'; @@ -59,9 +58,10 @@ export const ListComponent = ({ node }: IListProps) => { const { formData, setValues } = useDataModelBindings(bindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); const groupBinding = item.dataModelBindings?.group; + const { setList, isRowChecked } = useSaveObjectToGroup(node); - const appendToList = FD.useAppendToList(); - const removeFromList = FD.useRemoveIndexFromList(); + /*const appendToList = FD.useAppendToList(); + const removeFromList = FD.useRemoveIndexFromList();*/ const tableHeadersToShowInMobile = Object.keys(tableHeaders).filter( (key) => !tableHeadersMobile || tableHeadersMobile.includes(key), @@ -79,7 +79,7 @@ export const ListComponent = ({ node }: IListProps) => { setValues(next); } - function isRowSelected(row: Row): boolean { + /*function isRowSelected(row: Row): boolean { if (groupBinding) { const rows = (formData?.group as Row[] | undefined) ?? []; return rows.some((selectedRow) => @@ -87,20 +87,21 @@ export const ListComponent = ({ node }: IListProps) => { ); } return JSON.stringify(selectedRow) === JSON.stringify(row); - } + }*/ const title = item.textResourceBindings?.title; const description = item.textResourceBindings?.description; const handleRowClick = (row: Row) => { if (groupBinding) { - handleSelectedCheckboxRow(row); + setList(row); + //handleSelectedCheckboxRow(row); } else { handleSelectedRadioRow({ selectedValue: row }); } }; - const handleSelectedCheckboxRow = (row: Row) => { + /*const handleSelectedCheckboxRow = (row: Row) => { if (!groupBinding) { return; } @@ -128,7 +129,7 @@ export const ListComponent = ({ node }: IListProps) => { newValue: { ...next }, }); } - }; + };*/ const renderListItems = (row: Row, tableHeaders: { [x: string]: string | undefined }) => tableHeadersToShowInMobile.map((key) => ( @@ -159,7 +160,7 @@ export const ListComponent = ({ node }: IListProps) => { onClick={() => handleRowClick(row)} value={JSON.stringify(row)} className={cn(classes.mobile)} - checked={isRowSelected(row)} + checked={isRowChecked(row)} > {renderListItems(row, tableHeaders)} @@ -186,7 +187,7 @@ export const ListComponent = ({ node }: IListProps) => { handleSelectedRadioRow({ selectedValue: row })} > {renderListItems(row, tableHeaders)} @@ -261,7 +262,7 @@ export const ListComponent = ({ node }: IListProps) => { > {groupBinding ? ( @@ -270,7 +271,7 @@ export const ListComponent = ({ node }: IListProps) => { aria-label={JSON.stringify(row)} onChange={() => {}} value={JSON.stringify(row)} - checked={isRowSelected(row)} + checked={isRowChecked(row)} name={node.id} /> ) : ( @@ -281,7 +282,7 @@ export const ListComponent = ({ node }: IListProps) => { handleSelectedRadioRow({ selectedValue: row }); }} value={JSON.stringify(row)} - checked={isRowSelected(row)} + checked={isRowChecked(row)} name={node.id} /> )} @@ -290,7 +291,7 @@ export const ListComponent = ({ node }: IListProps) => { {typeof row[key] === 'string' ? : row[key]} From 82409e3a5b7e8aa24bcb7bb2b2f75d8adfb2c61b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Wed, 2 Apr 2025 09:50:50 +0200 Subject: [PATCH 31/59] Rename to checked from isDeleted --- src/features/saveToList/useSaveObjectToGroup.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/features/saveToList/useSaveObjectToGroup.ts b/src/features/saveToList/useSaveObjectToGroup.ts index 82c93f7fa8..35f12abf60 100644 --- a/src/features/saveToList/useSaveObjectToGroup.ts +++ b/src/features/saveToList/useSaveObjectToGroup.ts @@ -46,11 +46,11 @@ export const useSaveObjectToGroup = (node: LayoutNode<'List' | 'Checkboxes' | 'M } if (isRowChecked(row)) { const index = getIndexFromFormDataRow(row); - if (checkedBinding) { + if (checkedBinding && checkedBindingSegments) { setLeafValue({ reference: { ...bindings.checked, - field: `${checkedBindingSegments?.join('.')}[${index}].${checkedBindingKey}`, + field: `${checkedBindingSegments.join('.')}[${index}].${checkedBindingKey}`, } as IDataModelReference, newValue: false, }); @@ -64,7 +64,8 @@ export const useSaveObjectToGroup = (node: LayoutNode<'List' | 'Checkboxes' | 'M } } else { const formDataObject = getObjectFromFormDataRow(row); - if (formDataObject && checkedBindingKey && formDataObject[checkedBindingKey]) { + + if (formDataObject && checkedBindingKey && checkedBindingKey in formDataObject) { const index = getIndexFromFormDataRow(row); setLeafValue({ reference: { @@ -85,7 +86,8 @@ export const useSaveObjectToGroup = (node: LayoutNode<'List' | 'Checkboxes' | 'M if (propertyName) { next[propertyName] = row[propertyName]; } - } else if (binding !== 'group' && binding !== 'label' && binding !== 'metadata') { + //TODO: excluding from List now?? + } else if (binding !== 'group' && binding !== 'checked' && binding !== 'label' && binding !== 'metadata') { next[binding] = row[binding]; } } From 017dbf5271526835c521c3344e75c0c5b4098f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Wed, 2 Apr 2025 10:14:44 +0200 Subject: [PATCH 32/59] Support group and soft deletion in List component --- src/language/texts/en.ts | 9 ++-- src/language/texts/nb.ts | 8 ++-- src/language/texts/nn.ts | 8 ++-- .../Checkboxes/CheckboxesLayoutValidator.tsx | 8 ++-- src/layout/Checkboxes/index.tsx | 10 ++--- src/layout/List/ListLayoutValidator.tsx | 42 +++++++++++++++++++ src/layout/List/config.ts | 10 +++++ src/layout/List/index.tsx | 6 +++ 8 files changed, 79 insertions(+), 22 deletions(-) create mode 100644 src/layout/List/ListLayoutValidator.tsx diff --git a/src/language/texts/en.ts b/src/language/texts/en.ts index bb7d73b050..e9b90a6c83 100644 --- a/src/language/texts/en.ts +++ b/src/language/texts/en.ts @@ -438,11 +438,10 @@ export function en() { "Data type '{0}' is marked as 'disallowUserCreate=true', but the subform component is configured with 'showAddButton=true'. This is a contradiction, as the user will never be permitted to perform the add-button operation.", file_upload_same_binding: 'There are multiple FileUpload components with the same data model binding. Each component must have a unique binding. Other components with the same binding: {0}', - deletion_strategy_no_save_to_list: - 'The fields deletionStrategy and checked can only be used together with group.', - save_to_list_no_deletion_strategy: 'When you have set group, you must also set deletionStrategy.', - soft_delete_no_is_deleted: 'When you have set deletionStrategy to soft, you must also set "checked".', - hard_delete_with_is_deleted: 'When you have set deletionStrategy to hard, you cannot set "checked".', + deletion_strategy_no_group: 'The fields deletionStrategy and checked can only be used together with group.', + group_no_deletion_strategy: 'When you have set group, you must also set deletionStrategy.', + soft_delete_no_checked: 'When you have set deletionStrategy to soft, you must also set "checked".', + hard_delete_with_checked: 'When you have set deletionStrategy to hard, you cannot set "checked".', }, version_error: { version_mismatch: 'Version mismatch', diff --git a/src/language/texts/nb.ts b/src/language/texts/nb.ts index d1f11f80fb..cc822d2c38 100644 --- a/src/language/texts/nb.ts +++ b/src/language/texts/nb.ts @@ -439,10 +439,10 @@ export function nb(): FixedLanguageList { "Datatype '{0}' er markert som 'disallowUserCreate=true', men underskjema-komponenten er konfigurert med 'showAddButton=true'. Dette er en motsetning, siden brukeren aldri vil få lov til å utføre handlingene bak legg-til knappen.", file_upload_same_binding: 'Det er flere filopplastingskomponenter med samme datamodell-binding. Hver komponent må ha en unik binding. Andre komponenter med samme binding: {0}', - deletion_strategy_no_save_to_list: 'Feltene deletionStrategy og checked kan kun brukes sammen med group.', - save_to_list_no_deletion_strategy: 'Når du har satt group må du også sette deletionStrategy.', - soft_delete_no_is_deleted: 'Når du har satt deletionStrategy til soft må du også sette "checked".', - hard_delete_with_is_deleted: 'Når du har satt deletionStrategy til hard kan du ikke sette "checked".', + deletion_strategy_no_group: 'Feltene deletionStrategy og checked kan kun brukes sammen med group.', + group_no_deletion_strategy: 'Når du har satt group må du også sette deletionStrategy.', + soft_delete_no_checked: 'Når du har satt deletionStrategy til soft må du også sette "checked".', + hard_delete_with_checked: 'Når du har satt deletionStrategy til hard kan du ikke sette "checked".', }, version_error: { version_mismatch: 'Versjonsfeil', diff --git a/src/language/texts/nn.ts b/src/language/texts/nn.ts index 3e8436b738..7e48c9869c 100644 --- a/src/language/texts/nn.ts +++ b/src/language/texts/nn.ts @@ -439,10 +439,10 @@ export function nn(): FixedLanguageList { "Datatype '{0}' er markert som 'disallowUserCreate=true', men underskjema-komponenten er konfigurert med 'showAddButton=true'. Dette er ei motseiing, Sidan brukaren aldri vil få lov til å utføre handlingane bak legg-til knappen.", file_upload_same_binding: 'Det er fleire filopplastingskomponentar med same datamodellbinding. Kvar komponent må ha ein unik binding. Andre komponentar med same binding: {0}', - deletion_strategy_no_save_to_list: 'Felta deletionStrategy og checked kan berre brukast saman med group.', - save_to_list_no_deletion_strategy: 'Når du har sett group, må du også setje deletionStrategy.', - soft_delete_no_is_deleted: 'Når du har sett deletionStrategy til soft, må du også setje checked.', - hard_delete_with_is_deleted: 'Når du har sett deletionStrategy til hard, kan du ikkje setje checked.', + deletion_strategy_no_group: 'Felta deletionStrategy og checked kan berre brukast saman med group.', + group_no_deletion_strategy: 'Når du har sett group, må du også setje deletionStrategy.', + soft_delete_no_checked: 'Når du har sett deletionStrategy til soft, må du også setje checked.', + hard_delete_with_checked: 'Når du har sett deletionStrategy til hard, kan du ikkje setje checked.', }, version_error: { version_mismatch: 'Versjonsfeil', diff --git a/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx b/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx index dc985f2c41..f29dc5b93d 100644 --- a/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx +++ b/src/layout/Checkboxes/CheckboxesLayoutValidator.tsx @@ -18,17 +18,17 @@ export function CheckboxesLayoutValidator(props: NodeValidationProps<'Checkboxes if (!group) { if (!!deletionStrategy || !!checkedBinding) { - error = langAsString('config_error.deletion_strategy_no_save_to_list'); + error = langAsString('config_error.deletion_strategy_no_group'); } } else if (group) { if (!deletionStrategy) { - error = langAsString('config_error.save_to_list_no_deletion_strategy'); + error = langAsString('config_error.group_no_deletion_strategy'); } if (deletionStrategy === 'soft' && !checkedBinding) { - error = langAsString('config_error.soft_delete_no_is_deleted'); + error = langAsString('config_error.soft_delete_no_checked'); } if (deletionStrategy === 'hard' && !!checkedBinding) { - error = langAsString('config_error.hard_delete_with_is_deleted'); + error = langAsString('config_error.hard_delete_with_checked'); } } diff --git a/src/layout/Checkboxes/index.tsx b/src/layout/Checkboxes/index.tsx index 80c7817b62..3397f0541f 100644 --- a/src/layout/Checkboxes/index.tsx +++ b/src/layout/Checkboxes/index.tsx @@ -71,7 +71,7 @@ export class Checkboxes extends CheckboxesDef { } } - const [newErrors] = this.validateDataModelBindingsAny(ctx, 'saveToList', ['array'], false); + const [newErrors] = this.validateDataModelBindingsAny(ctx, 'group', ['array'], false); if (newErrors) { errors.push(...(newErrors || [])); } @@ -80,19 +80,19 @@ export class Checkboxes extends CheckboxesDef { const isCompatible = dataModelBindings?.simpleBinding?.field.includes(`${dataModelBindings.group.field}.`); if (!isCompatible) { - errors.push(`simpleBinding must reference a field in saveToList`); + errors.push(`simpleBinding must reference a field in group`); } const simpleBindingPath = dataModelBindings.simpleBinding?.field.split('.'); - const saveToListBinding = ctx.lookupBinding(dataModelBindings?.group); - const items = saveToListBinding[0]?.items; + const groupBinding = ctx.lookupBinding(dataModelBindings?.group); + const items = groupBinding[0]?.items; const properties = items && !Array.isArray(items) && typeof items === 'object' && 'properties' in items ? items.properties : undefined; if (!(properties && simpleBindingPath[1] in properties)) { - errors.push(`The property ${simpleBindingPath[1]} must be present in saveToList`); + errors.push(`The property ${simpleBindingPath[1]} must be present in group`); } } diff --git a/src/layout/List/ListLayoutValidator.tsx b/src/layout/List/ListLayoutValidator.tsx new file mode 100644 index 0000000000..df396745af --- /dev/null +++ b/src/layout/List/ListLayoutValidator.tsx @@ -0,0 +1,42 @@ +import { useEffect } from 'react'; + +import { useLanguage } from 'src/features/language/useLanguage'; +import { NodesInternal } from 'src/utils/layout/NodesContext'; +import type { NodeValidationProps } from 'src/layout/layout'; + +export function ListLayoutValidator(props: NodeValidationProps<'List'>) { + const { node, externalItem } = props; + const { langAsString } = useLanguage(); + const group = externalItem.dataModelBindings?.group; + const deletionStrategy = externalItem.deletionStrategy; + const checkedBinding = externalItem.dataModelBindings?.checked; + + const addError = NodesInternal.useAddError(); + + useEffect(() => { + let error: string | null = null; + + if (!group) { + if (!!deletionStrategy || !!checkedBinding) { + error = langAsString('config_error.deletion_strategy_no_group'); + } + } else if (group) { + if (!deletionStrategy) { + error = langAsString('config_error.group_no_deletion_strategy'); + } + if (deletionStrategy === 'soft' && !checkedBinding) { + error = langAsString('config_error.soft_delete_no_checked'); + } + if (deletionStrategy === 'hard' && !!checkedBinding) { + error = langAsString('config_error.hard_delete_with_checked'); + } + } + + if (error) { + addError(error, node); + window.logErrorOnce(`Validation error for '${node.id}': ${error}`); + } + }, [addError, node, deletionStrategy, checkedBinding, langAsString, group]); + + return null; +} diff --git a/src/layout/List/config.ts b/src/layout/List/config.ts index 9ab21b985c..329c44d7ab 100644 --- a/src/layout/List/config.ts +++ b/src/layout/List/config.ts @@ -36,11 +36,21 @@ export const Config = new CG.component({ ) .optional(), ), + new CG.prop( + 'checked', + new CG.dataModelBinding() + .setTitle('checked') + .setDescription( + 'If deletionStrategy=soft and group is set, this value points to where you want to save deleted status.', + ) + .optional(), + ), ) .optional() .additionalProperties(new CG.dataModelBinding().optional()) .exportAs('IDataModelBindingsForList'), ) + .addProperty(new CG.prop('deletionStrategy', new CG.enum('soft', 'hard').optional())) .addProperty( new CG.prop( 'tableHeaders', diff --git a/src/layout/List/index.tsx b/src/layout/List/index.tsx index d3e24df143..a7e48be2ba 100644 --- a/src/layout/List/index.tsx +++ b/src/layout/List/index.tsx @@ -8,6 +8,7 @@ import { useDisplayData } from 'src/features/displayData/useDisplayData'; import { evalQueryParameters } from 'src/features/options/evalQueryParameters'; import { ListDef } from 'src/layout/List/config.def.generated'; import { ListComponent } from 'src/layout/List/ListComponent'; +import { ListLayoutValidator } from 'src/layout/List/ListLayoutValidator'; import { ListSummary } from 'src/layout/List/ListSummary'; import { useValidateListIsEmpty } from 'src/layout/List/useValidateListIsEmpty'; import { SummaryItemSimple } from 'src/layout/Summary/SummaryItemSimple'; @@ -17,6 +18,7 @@ import type { LayoutValidationCtx } from 'src/features/devtools/layoutValidation import type { ComponentValidation } from 'src/features/validation'; import type { PropsFromGenericComponent } from 'src/layout'; import type { IDataModelReference } from 'src/layout/common.generated'; +import type { NodeValidationProps } from 'src/layout/layout'; import type { ExprResolver, SummaryRendererProps } from 'src/layout/LayoutComponent'; import type { Summary2Props } from 'src/layout/Summary2/SummaryComponent2/types'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; @@ -92,6 +94,10 @@ export class List extends ListDef { return useValidateListIsEmpty(node); } + renderLayoutValidators(props: NodeValidationProps<'List'>): JSX.Element | null { + return ; + } + validateDataModelBindings(ctx: LayoutValidationCtx<'List'>): string[] { const errors: string[] = []; const allowedLeafTypes = ['string', 'boolean', 'number', 'integer']; From d0d471de7b32f3a20b5a200c30a5de151ceca426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Wed, 2 Apr 2025 13:15:35 +0200 Subject: [PATCH 33/59] Support group and soft deletion in MultipleSelect component --- .../MultipleSelectComponent.tsx | 38 ++++++++++++++-- .../MultipleSelectLayoutValidator.tsx | 42 +++++++++++++++++ src/layout/MultipleSelect/config.ts | 26 ++++++++++- src/layout/MultipleSelect/index.tsx | 45 ++++++++++++++++++- 4 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 src/layout/MultipleSelect/MultipleSelectLayoutValidator.tsx diff --git a/src/layout/MultipleSelect/MultipleSelectComponent.tsx b/src/layout/MultipleSelect/MultipleSelectComponent.tsx index 2a5310ce61..7f97d28bfc 100644 --- a/src/layout/MultipleSelect/MultipleSelectComponent.tsx +++ b/src/layout/MultipleSelect/MultipleSelectComponent.tsx @@ -9,9 +9,12 @@ import { getDescriptionId } from 'src/components/label/Label'; import { DeleteWarningPopover } from 'src/features/alertOnChange/DeleteWarningPopover'; import { useAlertOnChange } from 'src/features/alertOnChange/useAlertOnChange'; import { FD } from 'src/features/formData/FormDataWrite'; +import { DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/types'; +import { useDataModelBindings } from 'src/features/formData/useDataModelBindings'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import { useGetOptions } from 'src/features/options/useGetOptions'; +import { useSaveObjectToGroup } from 'src/features/saveToList/useSaveObjectToGroup'; import { useIsValid } from 'src/features/validation/selectors/isValid'; import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; import comboboxClasses from 'src/styles/combobox.module.css'; @@ -20,12 +23,26 @@ import { useNodeItem } from 'src/utils/layout/useNodeItem'; import { optionSearchFilter } from 'src/utils/options'; import type { PropsFromGenericComponent } from 'src/layout'; +type Row = Record; + export type IMultipleSelectProps = PropsFromGenericComponent<'MultipleSelect'>; export function MultipleSelectComponent({ node, overrideDisplay }: IMultipleSelectProps) { const item = useNodeItem(node); const isValid = useIsValid(node); - const { id, readOnly, textResourceBindings, alertOnChange, grid, required, autocomplete } = item; + const { id, readOnly, textResourceBindings, alertOnChange, grid, required, dataModelBindings, autocomplete } = item; const { options, isFetching, selectedValues, setData } = useGetOptions(node, 'multi'); + const { formData } = useDataModelBindings(dataModelBindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); + const groupBinding = dataModelBindings.group; + const { isRowChecked, setList } = useSaveObjectToGroup(node); + const rowKey = dataModelBindings.simpleBinding.field.split('.').pop(); + const selectedGroupItems = (formData?.group as Row[]) + ?.map((row) => { + if (rowKey && isRowChecked(row)) { + return row[rowKey].toString(); + } + }) + .filter((el) => el !== undefined); + const debounce = FD.useDebounceImmediately(); const { langAsString, lang } = useLanguage(node); @@ -44,9 +61,23 @@ export function MultipleSelectComponent({ node, overrideDisplay }: IMultipleSele [lang, langAsString, options, selectedValues], ); + const handleOnChange = (values: string[]) => { + if (groupBinding) { + const newValue = + values > selectedGroupItems + ? values.find((value) => !selectedGroupItems.includes(value)) + : selectedGroupItems.find((value) => !values.includes(value)); + if (rowKey && newValue) { + setList({ [rowKey]: newValue }); + } + } else { + setData(values); + } + }; + const { alertOpen, setAlertOpen, handleChange, confirmChange, cancelChange, alertMessage } = useAlertOnChange( Boolean(alertOnChange), - setData, + handleOnChange, // Only alert when removing values (values) => values.length < selectedValues.length, changeMessageGenerator, @@ -55,7 +86,6 @@ export function MultipleSelectComponent({ node, overrideDisplay }: IMultipleSele if (isFetching) { return ; } - return ( ) { + const { node, externalItem } = props; + const { langAsString } = useLanguage(); + const group = externalItem.dataModelBindings.group; + const deletionStrategy = externalItem.deletionStrategy; + const checkedBinding = externalItem.dataModelBindings.checked; + + const addError = NodesInternal.useAddError(); + + useEffect(() => { + let error: string | null = null; + + if (!group) { + if (!!deletionStrategy || !!checkedBinding) { + error = langAsString('config_error.deletion_strategy_no_group'); + } + } else if (group) { + if (!deletionStrategy) { + error = langAsString('config_error.group_no_deletion_strategy'); + } + if (deletionStrategy === 'soft' && !checkedBinding) { + error = langAsString('config_error.soft_delete_no_checked'); + } + if (deletionStrategy === 'hard' && !!checkedBinding) { + error = langAsString('config_error.hard_delete_with_checked'); + } + } + + if (error) { + addError(error, node); + window.logErrorOnce(`Validation error for '${node.id}': ${error}`); + } + }, [addError, node, deletionStrategy, checkedBinding, langAsString, group]); + + return null; +} diff --git a/src/layout/MultipleSelect/config.ts b/src/layout/MultipleSelect/config.ts index 3488a64317..229e086f3f 100644 --- a/src/layout/MultipleSelect/config.ts +++ b/src/layout/MultipleSelect/config.ts @@ -53,6 +53,30 @@ export const Config = new CG.component({ ), ), ) - .addDataModelBinding(CG.common('IDataModelBindingsOptionsSimple')) + .addDataModelBinding( + new CG.obj( + new CG.prop( + 'group', + new CG.dataModelBinding() + .setTitle('group') + .setDescription( + 'Dot notation location for a repeating structure (array of objects), where you want to save the content of checked checkboxes', + ) + .optional(), + ), + new CG.prop( + 'checked', + new CG.dataModelBinding() + .setTitle('checked') + .setDescription( + 'If deletionStrategy=soft and group is set, this value points to where you want to save deleted status.', + ) + .optional(), + ), + ) + .exportAs('IDataModelBindingsForGroupMultiselect') + .extends(CG.common('IDataModelBindingsOptionsSimple')), + ) + .addProperty(new CG.prop('deletionStrategy', new CG.enum('soft', 'hard').optional())) .extends(CG.common('LabeledComponentProps')) .extendTextResources(CG.common('TRBLabel')); diff --git a/src/layout/MultipleSelect/index.tsx b/src/layout/MultipleSelect/index.tsx index 934da2dd8c..f2463bdcdb 100644 --- a/src/layout/MultipleSelect/index.tsx +++ b/src/layout/MultipleSelect/index.tsx @@ -8,11 +8,13 @@ import { useEmptyFieldValidationOnlySimpleBinding } from 'src/features/validatio import { MultipleChoiceSummary } from 'src/layout/Checkboxes/MultipleChoiceSummary'; import { MultipleSelectDef } from 'src/layout/MultipleSelect/config.def.generated'; import { MultipleSelectComponent } from 'src/layout/MultipleSelect/MultipleSelectComponent'; +import { MultipleSelectLayoutValidator } from 'src/layout/MultipleSelect/MultipleSelectLayoutValidator'; import { MultipleSelectSummary } from 'src/layout/MultipleSelect/MultipleSelectSummary'; import { useNodeFormDataWhenType } from 'src/utils/layout/useNodeItem'; import type { LayoutValidationCtx } from 'src/features/devtools/layoutValidation/types'; import type { ComponentValidation } from 'src/features/validation'; import type { PropsFromGenericComponent } from 'src/layout'; +import type { NodeValidationProps } from 'src/layout/layout'; import type { SummaryRendererProps } from 'src/layout/LayoutComponent'; import type { Summary2Props } from 'src/layout/Summary2/SummaryComponent2/types'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; @@ -51,7 +53,48 @@ export class MultipleSelect extends MultipleSelectDef { return useEmptyFieldValidationOnlySimpleBinding(node); } + renderLayoutValidators(props: NodeValidationProps<'MultipleSelect'>): JSX.Element | null { + return ; + } + validateDataModelBindings(ctx: LayoutValidationCtx<'MultipleSelect'>): string[] { - return this.validateDataModelBindingsSimple(ctx); + const errors: string[] = []; + const allowedTypes = ['string', 'boolean', 'number', 'integer']; + + const dataModelBindings = ctx.item.dataModelBindings ?? {}; + + if (!dataModelBindings?.group) { + for (const [binding] of Object.entries(dataModelBindings ?? {})) { + const [newErrors] = this.validateDataModelBindingsAny(ctx, binding, allowedTypes, false); + errors.push(...(newErrors || [])); + } + } + + const [newErrors] = this.validateDataModelBindingsAny(ctx, 'group', ['array'], false); + if (newErrors) { + errors.push(...(newErrors || [])); + } + + if (dataModelBindings?.group) { + const isCompatible = dataModelBindings?.simpleBinding?.field.includes(`${dataModelBindings.group.field}.`); + + if (!isCompatible) { + errors.push(`simpleBinding must reference a field in group`); + } + + const simpleBindingPath = dataModelBindings.simpleBinding?.field.split('.'); + const groupBinding = ctx.lookupBinding(dataModelBindings?.group); + const items = groupBinding[0]?.items; + const properties = + items && !Array.isArray(items) && typeof items === 'object' && 'properties' in items + ? items.properties + : undefined; + + if (!(properties && simpleBindingPath[1] in properties)) { + errors.push(`The property ${simpleBindingPath[1]} must be present in group`); + } + } + + return errors; } } From 8f785750e526cdbde2cff524611efc3fa3b5b434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Fri, 4 Apr 2025 08:57:20 +0200 Subject: [PATCH 34/59] Support group and soft deletion in MultipleSelect component --- src/features/options/useGetOptions.ts | 1 + .../CheckboxesContainerComponent.tsx | 3 +- src/layout/List/ListComponent.tsx | 43 ------------------- .../MultipleSelectComponent.tsx | 2 +- 4 files changed, 3 insertions(+), 46 deletions(-) diff --git a/src/features/options/useGetOptions.ts b/src/features/options/useGetOptions.ts index ad94279070..fb79d9d95d 100644 --- a/src/features/options/useGetOptions.ts +++ b/src/features/options/useGetOptions.ts @@ -92,6 +92,7 @@ export function useSetOptions( const setData = useCallback( (values: string[]) => { + console.log(valueType); if (valueType === 'single') { setValue('simpleBinding', values.at(0)); } else if (valueType === 'multi') { diff --git a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx index 5473fc6876..76e371b8b1 100644 --- a/src/layout/Checkboxes/CheckboxesContainerComponent.tsx +++ b/src/layout/Checkboxes/CheckboxesContainerComponent.tsx @@ -48,11 +48,10 @@ export const CheckboxContainerComponent = ({ node, overrideDisplay }: ICheckboxC const rowKey = dataModelBindings.simpleBinding.field.split('.').pop(); const setChecked = (isChecked: boolean, option) => { - const newData = isChecked ? [...selectedValues, option.value] : selectedValues.filter((o) => o !== option.value); - if (group) { setList({ [dataModelBindings.simpleBinding.field.split('.')[1]]: option.value }); } else { + const newData = isChecked ? [...selectedValues, option.value] : selectedValues.filter((o) => o !== option.value); setData(newData); } }; diff --git a/src/layout/List/ListComponent.tsx b/src/layout/List/ListComponent.tsx index c2c7934114..19a1695db5 100644 --- a/src/layout/List/ListComponent.tsx +++ b/src/layout/List/ListComponent.tsx @@ -60,9 +60,6 @@ export const ListComponent = ({ node }: IListProps) => { const groupBinding = item.dataModelBindings?.group; const { setList, isRowChecked } = useSaveObjectToGroup(node); - /*const appendToList = FD.useAppendToList(); - const removeFromList = FD.useRemoveIndexFromList();*/ - const tableHeadersToShowInMobile = Object.keys(tableHeaders).filter( (key) => !tableHeadersMobile || tableHeadersMobile.includes(key), ); @@ -79,16 +76,6 @@ export const ListComponent = ({ node }: IListProps) => { setValues(next); } - /*function isRowSelected(row: Row): boolean { - if (groupBinding) { - const rows = (formData?.group as Row[] | undefined) ?? []; - return rows.some((selectedRow) => - Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]), - ); - } - return JSON.stringify(selectedRow) === JSON.stringify(row); - }*/ - const title = item.textResourceBindings?.title; const description = item.textResourceBindings?.description; @@ -101,36 +88,6 @@ export const ListComponent = ({ node }: IListProps) => { } }; - /*const handleSelectedCheckboxRow = (row: Row) => { - if (!groupBinding) { - return; - } - if (isRowSelected(row)) { - const index = (formData?.group as Row[]).findIndex((selectedRow) => { - const { altinnRowId: _ } = selectedRow; - return Object.keys(row).every((key) => Object.hasOwn(selectedRow, key) && row[key] === selectedRow[key]); - }); - if (index >= 0) { - removeFromList({ - reference: groupBinding, - index, - }); - } - } else { - const uuid = uuidv4(); - const next: Row = { [ALTINN_ROW_ID]: uuid }; - for (const binding of Object.keys(bindings)) { - if (binding !== 'group') { - next[binding] = row[binding]; - } - } - appendToList({ - reference: groupBinding, - newValue: { ...next }, - }); - } - };*/ - const renderListItems = (row: Row, tableHeaders: { [x: string]: string | undefined }) => tableHeadersToShowInMobile.map((key) => (
diff --git a/src/layout/MultipleSelect/MultipleSelectComponent.tsx b/src/layout/MultipleSelect/MultipleSelectComponent.tsx index 7f97d28bfc..cf500bc536 100644 --- a/src/layout/MultipleSelect/MultipleSelectComponent.tsx +++ b/src/layout/MultipleSelect/MultipleSelectComponent.tsx @@ -79,7 +79,7 @@ export function MultipleSelectComponent({ node, overrideDisplay }: IMultipleSele Boolean(alertOnChange), handleOnChange, // Only alert when removing values - (values) => values.length < selectedValues.length, + (values) => (groupBinding ? values.length < selectedGroupItems?.length : values.length < selectedValues.length), changeMessageGenerator, ); From b55fe68ade4c84b6a869157e2545b0a016561fac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Fri, 4 Apr 2025 08:58:07 +0200 Subject: [PATCH 35/59] Support group and soft deletion in MultipleSelect component --- src/features/options/useGetOptions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/options/useGetOptions.ts b/src/features/options/useGetOptions.ts index fb79d9d95d..ad94279070 100644 --- a/src/features/options/useGetOptions.ts +++ b/src/features/options/useGetOptions.ts @@ -92,7 +92,6 @@ export function useSetOptions( const setData = useCallback( (values: string[]) => { - console.log(valueType); if (valueType === 'single') { setValue('simpleBinding', values.at(0)); } else if (valueType === 'multi') { From da6630fda7913f8edabd270f21b390561f3db046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Fri, 4 Apr 2025 10:13:08 +0200 Subject: [PATCH 36/59] Support group and soft deletion in MultipleSelect component --- src/layout/Checkboxes/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/layout/Checkboxes/index.tsx b/src/layout/Checkboxes/index.tsx index 3397f0541f..6d0f8e99e6 100644 --- a/src/layout/Checkboxes/index.tsx +++ b/src/layout/Checkboxes/index.tsx @@ -72,9 +72,7 @@ export class Checkboxes extends CheckboxesDef { } const [newErrors] = this.validateDataModelBindingsAny(ctx, 'group', ['array'], false); - if (newErrors) { - errors.push(...(newErrors || [])); - } + errors.push(...(newErrors || [])); if (dataModelBindings?.group) { const isCompatible = dataModelBindings?.simpleBinding?.field.includes(`${dataModelBindings.group.field}.`); From 40469226f926809d9a59842888288d49adefa30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Fri, 4 Apr 2025 10:15:18 +0200 Subject: [PATCH 37/59] Support group and soft deletion in MultipleSelect component --- src/layout/MultipleSelect/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/layout/MultipleSelect/index.tsx b/src/layout/MultipleSelect/index.tsx index f2463bdcdb..a828b1991e 100644 --- a/src/layout/MultipleSelect/index.tsx +++ b/src/layout/MultipleSelect/index.tsx @@ -71,9 +71,7 @@ export class MultipleSelect extends MultipleSelectDef { } const [newErrors] = this.validateDataModelBindingsAny(ctx, 'group', ['array'], false); - if (newErrors) { - errors.push(...(newErrors || [])); - } + errors.push(...(newErrors || [])); if (dataModelBindings?.group) { const isCompatible = dataModelBindings?.simpleBinding?.field.includes(`${dataModelBindings.group.field}.`); From 45d1bec7bfe5739f2c32d8fded3b65b07edc5f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Mon, 7 Apr 2025 13:56:09 +0200 Subject: [PATCH 38/59] Fix test --- src/app-components/Label/Label.tsx | 34 +++++++++++-------- src/components/label/LabelContent.tsx | 23 ++++++------- .../DeleteWarningPopover.module.css | 3 ++ .../alertOnChange/DeleteWarningPopover.tsx | 2 +- src/layout/Checkboxes/index.tsx | 8 ++--- src/layout/MultipleSelect/index.tsx | 8 ++--- 6 files changed, 37 insertions(+), 41 deletions(-) diff --git a/src/app-components/Label/Label.tsx b/src/app-components/Label/Label.tsx index 3e082dc60e..4fd71bd289 100644 --- a/src/app-components/Label/Label.tsx +++ b/src/app-components/Label/Label.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import type { JSX, PropsWithChildren, ReactElement } from 'react'; import { Label as DesignsystemetLabel } from '@digdir/designsystemet-react'; @@ -22,19 +22,22 @@ type LabelProps = { style?: DesignsystemetLabelProps['style']; }; -export function Label({ - label, - htmlFor, - required, - requiredIndicator, - optionalIndicator, - help, - description, - className, - grid, - style, - children, -}: PropsWithChildren) { +export const Label = forwardRef>(function Label( + { + label, + htmlFor, + required, + requiredIndicator, + optionalIndicator, + help, + description, + className, + grid, + style, + children, + }, + ref, +) { if (!label) { return children; } @@ -52,6 +55,7 @@ export function Label({ ); -} +}); diff --git a/src/components/label/LabelContent.tsx b/src/components/label/LabelContent.tsx index 964f2eff7a..4bf89cfc5e 100644 --- a/src/components/label/LabelContent.tsx +++ b/src/components/label/LabelContent.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { forwardRef } from 'react'; import cn from 'classnames'; @@ -23,16 +23,10 @@ export type LabelContentProps = Readonly<{ labelSettings?: ILabelSettings; }> & { className?: string }; -export function LabelContent({ - componentId, - label, - description, - required, - readOnly, - help, - labelSettings, - className, -}: LabelContentProps) { +export const LabelContent = forwardRef(function LabelContent( + { componentId, label, description, required, readOnly, help, labelSettings, className }, + ref, +) { const { overrideDisplay } = useFormComponentCtx() ?? {}; const { elementAsString } = useLanguage(); @@ -41,7 +35,10 @@ export function LabelContent({ } return ( - + {typeof label === 'string' ? : label} @@ -70,4 +67,4 @@ export function LabelContent({ )} ); -} +}); diff --git a/src/features/alertOnChange/DeleteWarningPopover.module.css b/src/features/alertOnChange/DeleteWarningPopover.module.css index 73cb5cde9e..eb3a485043 100644 --- a/src/features/alertOnChange/DeleteWarningPopover.module.css +++ b/src/features/alertOnChange/DeleteWarningPopover.module.css @@ -1,3 +1,6 @@ +.popoverContent { + z-index: 1700; +} .popoverButtonContainer { display: flex; flex-direction: row; diff --git a/src/features/alertOnChange/DeleteWarningPopover.tsx b/src/features/alertOnChange/DeleteWarningPopover.tsx index 3ef5461ee2..d4c037210a 100644 --- a/src/features/alertOnChange/DeleteWarningPopover.tsx +++ b/src/features/alertOnChange/DeleteWarningPopover.tsx @@ -35,7 +35,7 @@ export function DeleteWarningPopover({ onOpenChange={() => setOpen(!open)} > {children} - +
{messageText}
)); - const options = groupBinding ? ( + const options = groupBinding.enabled ? ( { } description={description && } > - {data?.listItems.map((row) => { - console.log(row); - return ( - handleRowClick(row)} - value={JSON.stringify(row)} - className={cn(classes.mobile)} - checked={isRowChecked(row)} - > - {renderListItems(row, tableHeaders)} - - ); - })} + {data?.listItems.map((row) => ( + handleRowClick(row)} + value={JSON.stringify(row)} + className={cn(classes.mobile)} + checked={groupBinding.isChecked(row)} + > + {renderListItems(row, tableHeaders)} + + ))} ) : ( { [classes.selectedRowCell]: isRowSelected(row), })} > - {groupBinding ? ( + {groupBinding.enabled ? ( {}} value={JSON.stringify(row)} - checked={isRowChecked(row)} + checked={groupBinding.isChecked(row)} name={node.id} /> ) : ( diff --git a/src/layout/MultipleSelect/MultipleSelectComponent.tsx b/src/layout/MultipleSelect/MultipleSelectComponent.tsx index e9679ee0c0..89c6d667a7 100644 --- a/src/layout/MultipleSelect/MultipleSelectComponent.tsx +++ b/src/layout/MultipleSelect/MultipleSelectComponent.tsx @@ -9,12 +9,10 @@ import { getDescriptionId } from 'src/components/label/Label'; import { DeleteWarningPopover } from 'src/features/alertOnChange/DeleteWarningPopover'; import { useAlertOnChange } from 'src/features/alertOnChange/useAlertOnChange'; import { FD } from 'src/features/formData/FormDataWrite'; -import { DEFAULT_DEBOUNCE_TIMEOUT } from 'src/features/formData/types'; -import { useDataModelBindings } from 'src/features/formData/useDataModelBindings'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import { useGetOptions } from 'src/features/options/useGetOptions'; -import { useSaveObjectToGroup } from 'src/features/saveToList/useSaveObjectToGroup'; +import { useSaveValueToGroup } from 'src/features/saveToGroup/useSaveToGroup'; import { useIsValid } from 'src/features/validation/selectors/isValid'; import { ComponentStructureWrapper } from 'src/layout/ComponentStructureWrapper'; import comboboxClasses from 'src/styles/combobox.module.css'; @@ -23,28 +21,14 @@ import { useNodeItem } from 'src/utils/layout/useNodeItem'; import { optionSearchFilter } from 'src/utils/options'; import type { PropsFromGenericComponent } from 'src/layout'; -type Row = Record; - export type IMultipleSelectProps = PropsFromGenericComponent<'MultipleSelect'>; export function MultipleSelectComponent({ node, overrideDisplay }: IMultipleSelectProps) { const item = useNodeItem(node); const isValid = useIsValid(node); const { id, readOnly, textResourceBindings, alertOnChange, grid, required, dataModelBindings } = item; - const { options, isFetching, selectedValues, setData } = useGetOptions(node, 'multi'); - const { formData } = useDataModelBindings(dataModelBindings, DEFAULT_DEBOUNCE_TIMEOUT, 'raw'); - const groupBinding = dataModelBindings.group; - const objectToGroupBindings = { ...dataModelBindings }; - delete objectToGroupBindings.label; - delete objectToGroupBindings.metadata; - const { isRowChecked, toggleRowSelectionInList } = useSaveObjectToGroup(objectToGroupBindings); - const rowKey = dataModelBindings.simpleBinding.field.split('.').pop(); - const selectedGroupItems = (formData?.group as Row[]) - ?.map((row) => { - if (rowKey && isRowChecked(row)) { - return row[rowKey].toString(); - } - }) - .filter((el) => el !== undefined); + const { options, isFetching, selectedValues: selectedFromSimpleBinding, setData } = useGetOptions(node, 'multi'); + const groupBinding = useSaveValueToGroup(dataModelBindings); + const selectedValues = groupBinding.enabled ? groupBinding.selectedValues : selectedFromSimpleBinding; const debounce = FD.useDebounceImmediately(); const { langAsString, lang } = useLanguage(node); @@ -65,14 +49,8 @@ export function MultipleSelectComponent({ node, overrideDisplay }: IMultipleSele ); const handleOnChange = (values: string[]) => { - if (groupBinding) { - const newValue = - values > selectedGroupItems - ? values.find((value) => !selectedGroupItems.includes(value)) - : selectedGroupItems.find((value) => !values.includes(value)); - if (rowKey && newValue) { - toggleRowSelectionInList({ [rowKey]: newValue }); - } + if (groupBinding.enabled) { + groupBinding.setCheckedValues(values); } else { setData(values); } @@ -82,7 +60,7 @@ export function MultipleSelectComponent({ node, overrideDisplay }: IMultipleSele Boolean(alertOnChange), handleOnChange, // Only alert when removing values - (values) => (groupBinding ? values.length < selectedGroupItems?.length : values.length < selectedValues.length), + (values) => values.length < selectedValues.length, changeMessageGenerator, ); @@ -123,7 +101,7 @@ export function MultipleSelectComponent({ node, overrideDisplay }: IMultipleSele id={id} filter={optionSearchFilter} size='sm' - value={groupBinding ? selectedGroupItems : selectedValues} + value={selectedValues} readOnly={readOnly} onValueChange={handleChange} onBlur={debounce} From 602ba609402bcd1f0207bac5a5b9e669792e8102 Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Wed, 30 Apr 2025 21:33:33 +0200 Subject: [PATCH 50/59] Fixing the mistake with value bindings as an array --- src/features/saveToGroup/useSaveToGroup.ts | 40 +++++++++------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/features/saveToGroup/useSaveToGroup.ts b/src/features/saveToGroup/useSaveToGroup.ts index ee38efe451..1ebb6b50c7 100644 --- a/src/features/saveToGroup/useSaveToGroup.ts +++ b/src/features/saveToGroup/useSaveToGroup.ts @@ -13,7 +13,7 @@ type Row = Record; interface Bindings { group?: IDataModelReference; checked?: IDataModelReference; - valueBindings: IDataModelReference[]; + values: Record; } function toRelativePath(group: IDataModelReference | undefined, binding: IDataModelReference | undefined) { @@ -23,10 +23,10 @@ function toRelativePath(group: IDataModelReference | undefined, binding: IDataMo return undefined; } -function isEqual({ group, valueBindings }: Bindings, row1: Row, row2: Row) { - for (const valueBinding of valueBindings) { - const path = toRelativePath(group, valueBinding); - if (path && dot.pick(path, row1) !== dot.pick(path, row2)) { +function isEqual({ group, values }: Bindings, source: Row, formDataRow: Row) { + for (const key in values) { + const path = toRelativePath(group, values[key]); + if (path && source[key] !== dot.pick(path, formDataRow)) { return false; } } @@ -49,7 +49,7 @@ function findRowInFormData( } function useSaveToGroup(bindings: Bindings) { - const { group, checked, valueBindings } = bindings; + const { group, checked, values } = bindings; const formData = FD.useFreshBindings(group ? { group } : {}, 'raw').group as Row[] | undefined; const setLeafValue = FD.useSetLeafValue(); const appendToList = FD.useAppendToList(); @@ -80,10 +80,10 @@ function useSaveToGroup(bindings: Bindings) { if (checkedPath) { dot.str(checkedPath, true, newRow); } - for (const valueBinding of valueBindings) { - const path = toRelativePath(group, valueBinding); + for (const key in values) { + const path = toRelativePath(group, values[key]); if (path) { - dot.str(path, dot.pick(path, row), newRow); + dot.str(path, row[key], newRow); } } appendToList({ reference: group, newValue: newRow }); @@ -99,14 +99,14 @@ function useSaveToGroup(bindings: Bindings) { * structure in the data model (aka object[]) */ export function useSaveObjectToGroup(listBindings: IDataModelBindingsForList) { - const valueBindings: IDataModelReference[] = []; + const values: Record = {}; for (const key in listBindings) { const binding = listBindings[key]; if (key !== 'group' && key !== 'checked' && binding) { - valueBindings.push(binding); + values[key] = binding; } } - const bindings: Bindings = { group: listBindings.group, checked: listBindings.checked, valueBindings }; + const bindings: Bindings = { group: listBindings.group, checked: listBindings.checked, values }; const { formData, enabled, toggle, checkedPath } = useSaveToGroup(bindings); function isChecked(row: Row) { @@ -129,7 +129,7 @@ export function useSaveValueToGroup( const { formData, enabled, toggle, checkedPath } = useSaveToGroup({ group: bindings.group, checked: bindings.checked, - valueBindings: bindings.simpleBinding ? [bindings.simpleBinding] : [], + values: bindings.simpleBinding ? { value: bindings.simpleBinding } : {}, }); const valuePath = toRelativePath(bindings.group, bindings.simpleBinding); @@ -141,17 +141,11 @@ export function useSaveValueToGroup( : []; function toggleValue(value: string) { - if (!valuePath || !enabled) { - return; - } - - const asRow: Row = {}; - dot.str(valuePath, value, asRow); - toggle(asRow); + enabled && toggle({ value }); } function setCheckedValues(values: string[]) { - if (!valuePath || !enabled) { + if (!enabled) { return; } @@ -160,9 +154,7 @@ export function useSaveValueToGroup( const valuesToToggle = [...valuesToSet, ...valuesToRemove]; for (const value of valuesToToggle) { - const asRow: Row = {}; - dot.str(valuePath, value, asRow); - toggle(asRow); + toggle({ value }); } } From dd58c978a6c72cf0c51d001dc34893e9f117ecbe Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Wed, 30 Apr 2025 21:37:42 +0200 Subject: [PATCH 51/59] This should fix the unselection bug in List --- src/features/saveToGroup/useSaveToGroup.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/saveToGroup/useSaveToGroup.ts b/src/features/saveToGroup/useSaveToGroup.ts index 1ebb6b50c7..16e00e9955 100644 --- a/src/features/saveToGroup/useSaveToGroup.ts +++ b/src/features/saveToGroup/useSaveToGroup.ts @@ -61,8 +61,8 @@ function useSaveToGroup(bindings: Bindings) { return; } - const [index] = findRowInFormData(bindings, row, formData); - const isChecked = !!(checkedPath ? dot.pick(checkedPath, row) : true); + const [index, formDataRow] = findRowInFormData(bindings, row, formData); + const isChecked = !!(checkedPath ? dot.pick(checkedPath, formDataRow) : true); if (isChecked) { if (checked && checkedPath) { const field = `${group.field}[${index}].${checkedPath}`; From a3a640ddd41eb9a49935cf7b31a081afbfd3eecd Mon Sep 17 00:00:00 2001 From: Ole Martin Handeland Date: Fri, 2 May 2025 16:30:06 +0200 Subject: [PATCH 52/59] Reverting updated snapshots. These snapshot tests should run without jest preview on, as the class names change when you turn on that feature (and will cause the tests to fail). Since jest preview is off on the github runner, we have to update them with preview off and commit that instead. --- .../SummaryGroupComponent.test.tsx.snap | 12 ++++++------ .../SummaryRepeatingGroup.test.tsx.snap | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/layout/Group/__snapshots__/SummaryGroupComponent.test.tsx.snap b/src/layout/Group/__snapshots__/SummaryGroupComponent.test.tsx.snap index 26c564a83e..b6437e21ed 100644 --- a/src/layout/Group/__snapshots__/SummaryGroupComponent.test.tsx.snap +++ b/src/layout/Group/__snapshots__/SummaryGroupComponent.test.tsx.snap @@ -12,10 +12,10 @@ exports[`SummaryGroupComponent SummaryGroupComponent -- should match snapshot 1` style="width: 100%;" >
Mock group @@ -23,7 +23,7 @@ exports[`SummaryGroupComponent SummaryGroupComponent -- should match snapshot 1`
+ + + +
diff --git a/src/layout/List/index.tsx b/src/layout/List/index.tsx index 53bae557f7..e5c096ba86 100644 --- a/src/layout/List/index.tsx +++ b/src/layout/List/index.tsx @@ -33,6 +33,7 @@ export class List extends ListDef { useDisplayData(nodeId: string): string { const dmBindings = NodesInternal.useNodeDataWhenType(nodeId, 'List', (data) => data.layout.dataModelBindings); const groupBinding = dmBindings?.group; + const checkedBinding = dmBindings?.checked; const summaryBinding = NodesInternal.useNodeDataWhenType(nodeId, 'List', (data) => data.item?.summaryBinding); const legacySummaryBinding = NodesInternal.useNodeDataWhenType( nodeId, @@ -46,9 +47,13 @@ export class List extends ListDef { // values are undefined. This is because useNodeFormDataWhenType doesn't know the intricacies of the group // binding and how it works in the List component. We need to find the values inside the rows ourselves. const rows = (formData?.group as unknown[] | undefined) ?? []; + const relativeCheckedBinding = checkedBinding?.field.replace(`${groupBinding.field}.`, ''); if (summaryBinding && dmBindings) { const summaryReference = dmBindings[summaryBinding]; - const rowData = rows.map((row) => (summaryReference ? findDataInRow(row, summaryReference, groupBinding) : '')); + const rowData = rows + .filter((row) => !relativeCheckedBinding || dot.pick(relativeCheckedBinding, row) === true) + .map((row) => (summaryReference ? findDataInRow(row, summaryReference, groupBinding) : '')); + return Object.values(rowData).join(', '); } From 043e914cb17ff51055d3ba7d2852b7a19ba1f00b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Mon, 5 May 2025 10:13:00 +0200 Subject: [PATCH 56/59] Move ObjectToGroupLayoutValidator to useSaveToGroup folder --- .../saveToGroup}/ObjectToGroupLayoutValidator.tsx | 0 src/layout/Checkboxes/index.tsx | 2 +- src/layout/List/index.tsx | 2 +- src/layout/MultipleSelect/index.tsx | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/{layout/List => features/saveToGroup}/ObjectToGroupLayoutValidator.tsx (100%) diff --git a/src/layout/List/ObjectToGroupLayoutValidator.tsx b/src/features/saveToGroup/ObjectToGroupLayoutValidator.tsx similarity index 100% rename from src/layout/List/ObjectToGroupLayoutValidator.tsx rename to src/features/saveToGroup/ObjectToGroupLayoutValidator.tsx diff --git a/src/layout/Checkboxes/index.tsx b/src/layout/Checkboxes/index.tsx index e1e0b59e15..6b03e4fe43 100644 --- a/src/layout/Checkboxes/index.tsx +++ b/src/layout/Checkboxes/index.tsx @@ -5,12 +5,12 @@ import { useLanguage } from 'src/features/language/useLanguage'; import { getCommaSeparatedOptionsToText } from 'src/features/options/getCommaSeparatedOptionsToText'; import { useNodeOptions } from 'src/features/options/useNodeOptions'; import { validateSimpleBindingWithOptionalGroup } from 'src/features/saveToGroup/layoutValidation'; +import { ObjectToGroupLayoutValidator } from 'src/features/saveToGroup/ObjectToGroupLayoutValidator'; import { useEmptyFieldValidationOnlySimpleBinding } from 'src/features/validation/nodeValidation/emptyFieldValidation'; import { CheckboxContainerComponent } from 'src/layout/Checkboxes/CheckboxesContainerComponent'; import { CheckboxesSummary } from 'src/layout/Checkboxes/CheckboxesSummary'; import { CheckboxesDef } from 'src/layout/Checkboxes/config.def.generated'; import { MultipleChoiceSummary } from 'src/layout/Checkboxes/MultipleChoiceSummary'; -import { ObjectToGroupLayoutValidator } from 'src/layout/List/ObjectToGroupLayoutValidator'; import { useNodeFormDataWhenType } from 'src/utils/layout/useNodeItem'; import type { LayoutValidationCtx } from 'src/features/devtools/layoutValidation/types'; import type { ComponentValidation } from 'src/features/validation'; diff --git a/src/layout/List/index.tsx b/src/layout/List/index.tsx index e5c096ba86..68024ae6ca 100644 --- a/src/layout/List/index.tsx +++ b/src/layout/List/index.tsx @@ -6,10 +6,10 @@ import dot from 'dot-object'; import { lookupErrorAsText } from 'src/features/datamodel/lookupErrorAsText'; import { useDisplayData } from 'src/features/displayData/useDisplayData'; import { evalQueryParameters } from 'src/features/options/evalQueryParameters'; +import { ObjectToGroupLayoutValidator } from 'src/features/saveToGroup/ObjectToGroupLayoutValidator'; import { ListDef } from 'src/layout/List/config.def.generated'; import { ListComponent } from 'src/layout/List/ListComponent'; import { ListSummary } from 'src/layout/List/ListSummary'; -import { ObjectToGroupLayoutValidator } from 'src/layout/List/ObjectToGroupLayoutValidator'; import { useValidateListIsEmpty } from 'src/layout/List/useValidateListIsEmpty'; import { SummaryItemSimple } from 'src/layout/Summary/SummaryItemSimple'; import { NodesInternal } from 'src/utils/layout/NodesContext'; diff --git a/src/layout/MultipleSelect/index.tsx b/src/layout/MultipleSelect/index.tsx index 01bcadf7ff..a7a1a6348e 100644 --- a/src/layout/MultipleSelect/index.tsx +++ b/src/layout/MultipleSelect/index.tsx @@ -5,9 +5,9 @@ import { useLanguage } from 'src/features/language/useLanguage'; import { getCommaSeparatedOptionsToText } from 'src/features/options/getCommaSeparatedOptionsToText'; import { useNodeOptions } from 'src/features/options/useNodeOptions'; import { validateSimpleBindingWithOptionalGroup } from 'src/features/saveToGroup/layoutValidation'; +import { ObjectToGroupLayoutValidator } from 'src/features/saveToGroup/ObjectToGroupLayoutValidator'; import { useEmptyFieldValidationOnlySimpleBinding } from 'src/features/validation/nodeValidation/emptyFieldValidation'; import { MultipleChoiceSummary } from 'src/layout/Checkboxes/MultipleChoiceSummary'; -import { ObjectToGroupLayoutValidator } from 'src/layout/List/ObjectToGroupLayoutValidator'; import { MultipleSelectDef } from 'src/layout/MultipleSelect/config.def.generated'; import { MultipleSelectComponent } from 'src/layout/MultipleSelect/MultipleSelectComponent'; import { MultipleSelectSummary } from 'src/layout/MultipleSelect/MultipleSelectSummary'; From df4f3eb0bbfaec9ca8a0ca90ae6bc7a53b1a39ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Mon, 5 May 2025 13:46:52 +0200 Subject: [PATCH 57/59] Add support for Summary and Summary2 when using group --- .../Checkboxes/MultipleChoiceSummary.tsx | 26 ++++++++++++-- src/layout/Checkboxes/index.tsx | 34 +++++++++++++++++-- src/layout/MultipleSelect/index.tsx | 34 +++++++++++++++++-- .../MultipleValueSummary.tsx | 26 ++++++++++++-- 4 files changed, 111 insertions(+), 9 deletions(-) diff --git a/src/layout/Checkboxes/MultipleChoiceSummary.tsx b/src/layout/Checkboxes/MultipleChoiceSummary.tsx index 9576b63968..c19e3e928c 100644 --- a/src/layout/Checkboxes/MultipleChoiceSummary.tsx +++ b/src/layout/Checkboxes/MultipleChoiceSummary.tsx @@ -1,23 +1,45 @@ import React from 'react'; +import dot from 'dot-object'; + import { Flex } from 'src/app-components/Flex/Flex'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; import { getCommaSeparatedOptionsToText } from 'src/features/options/getCommaSeparatedOptionsToText'; import { useNodeOptions } from 'src/features/options/useNodeOptions'; import classes from 'src/layout/Checkboxes/MultipleChoiceSummary.module.css'; -import { useNodeFormData } from 'src/utils/layout/useNodeItem'; +import { useNodeFormData, useNodeItem } from 'src/utils/layout/useNodeItem'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; +type Row = Record; + export interface IMultipleChoiceSummaryProps { targetNode: LayoutNode<'Checkboxes' | 'MultipleSelect'>; } export function MultipleChoiceSummary({ targetNode }: IMultipleChoiceSummaryProps) { const rawFormData = useNodeFormData(targetNode); + const { dataModelBindings } = useNodeItem(targetNode); const options = useNodeOptions(targetNode).options; const { langAsString } = useLanguage(); - const data = getCommaSeparatedOptionsToText(rawFormData.simpleBinding, options, langAsString); + + const relativeCheckedPath = + dataModelBindings?.checked && dataModelBindings?.group + ? dataModelBindings.checked.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const relativeSimpleBindingPath = + dataModelBindings?.simpleBinding && dataModelBindings?.group + ? dataModelBindings.simpleBinding.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const displayRows = (rawFormData?.group as unknown as Row[]) + ?.filter((row) => (!relativeCheckedPath ? true : dot.pick(relativeCheckedPath, row) === true)) + .map((row) => (!relativeSimpleBindingPath ? true : dot.pick(relativeSimpleBindingPath, row))); + + const data = dataModelBindings.group + ? displayRows + : getCommaSeparatedOptionsToText(rawFormData.simpleBinding, options, langAsString); return ( ; + export class Checkboxes extends CheckboxesDef { render = forwardRef>( function LayoutComponentCheckboxesRender(props, _): JSX.Element | null { @@ -29,10 +34,35 @@ export class Checkboxes extends CheckboxesDef { ); useDisplayData(nodeId: string): string { + const node = useNode(nodeId); const formData = useNodeFormDataWhenType(nodeId, 'Checkboxes'); const options = useNodeOptions(nodeId).options; const langAsString = useLanguage().langAsString; - const data = getCommaSeparatedOptionsToText(formData?.simpleBinding, options, langAsString); + + if (!node) { + return ''; + } + + const dataModelBindings = useNodeItem(node as LayoutNode<'Checkboxes'>, (i) => i.dataModelBindings); + + const relativeCheckedPath = + dataModelBindings?.checked && dataModelBindings?.group + ? dataModelBindings.checked.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const relativeSimpleBindingPath = + dataModelBindings?.simpleBinding && dataModelBindings?.group + ? dataModelBindings.simpleBinding.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const displayRows = (formData?.group as unknown as Row[]) + ?.filter((row) => (!relativeCheckedPath ? true : dot.pick(relativeCheckedPath, row) === true)) + .map((row) => (!relativeSimpleBindingPath ? true : dot.pick(relativeSimpleBindingPath, row))); + + const data = dataModelBindings.group + ? displayRows + : getCommaSeparatedOptionsToText(formData?.simpleBinding, options, langAsString); + return Object.values(data).join(', '); } diff --git a/src/layout/MultipleSelect/index.tsx b/src/layout/MultipleSelect/index.tsx index a7a1a6348e..c6b0e8ea12 100644 --- a/src/layout/MultipleSelect/index.tsx +++ b/src/layout/MultipleSelect/index.tsx @@ -1,6 +1,8 @@ import React, { forwardRef } from 'react'; import type { JSX } from 'react'; +import dot from 'dot-object'; + import { useLanguage } from 'src/features/language/useLanguage'; import { getCommaSeparatedOptionsToText } from 'src/features/options/getCommaSeparatedOptionsToText'; import { useNodeOptions } from 'src/features/options/useNodeOptions'; @@ -11,7 +13,8 @@ import { MultipleChoiceSummary } from 'src/layout/Checkboxes/MultipleChoiceSumma import { MultipleSelectDef } from 'src/layout/MultipleSelect/config.def.generated'; import { MultipleSelectComponent } from 'src/layout/MultipleSelect/MultipleSelectComponent'; import { MultipleSelectSummary } from 'src/layout/MultipleSelect/MultipleSelectSummary'; -import { useNodeFormDataWhenType } from 'src/utils/layout/useNodeItem'; +import { useNode } from 'src/utils/layout/NodesContext'; +import { useNodeFormDataWhenType, useNodeItem } from 'src/utils/layout/useNodeItem'; import type { LayoutValidationCtx } from 'src/features/devtools/layoutValidation/types'; import type { ComponentValidation } from 'src/features/validation'; import type { PropsFromGenericComponent } from 'src/layout'; @@ -20,6 +23,8 @@ import type { SummaryRendererProps } from 'src/layout/LayoutComponent'; import type { Summary2Props } from 'src/layout/Summary2/SummaryComponent2/types'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; +type Row = Record; + export class MultipleSelect extends MultipleSelectDef { render = forwardRef>( function LayoutComponentMultipleSelectRender(props, _): JSX.Element | null { @@ -28,10 +33,35 @@ export class MultipleSelect extends MultipleSelectDef { ); useDisplayData(nodeId: string): string { + const node = useNode(nodeId); const formData = useNodeFormDataWhenType(nodeId, 'MultipleSelect'); const options = useNodeOptions(nodeId).options; const langAsString = useLanguage().langAsString; - const data = getCommaSeparatedOptionsToText(formData?.simpleBinding, options, langAsString); + + if (!node) { + return ''; + } + + const dataModelBindings = useNodeItem(node as LayoutNode<'MultipleSelect'>, (i) => i.dataModelBindings); + + const relativeCheckedPath = + dataModelBindings?.checked && dataModelBindings?.group + ? dataModelBindings.checked.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const relativeSimpleBindingPath = + dataModelBindings?.simpleBinding && dataModelBindings?.group + ? dataModelBindings.simpleBinding.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const displayRows = (formData?.group as unknown as Row[]) + ?.filter((row) => (!relativeCheckedPath ? true : dot.pick(relativeCheckedPath, row) === true)) + .map((row) => (!relativeSimpleBindingPath ? true : dot.pick(relativeSimpleBindingPath, row))); + + const data = dataModelBindings.group + ? displayRows + : getCommaSeparatedOptionsToText(formData?.simpleBinding, options, langAsString); + return Object.values(data).join(', '); } diff --git a/src/layout/Summary2/CommonSummaryComponents/MultipleValueSummary.tsx b/src/layout/Summary2/CommonSummaryComponents/MultipleValueSummary.tsx index 095c92e01e..4720eebc2d 100644 --- a/src/layout/Summary2/CommonSummaryComponents/MultipleValueSummary.tsx +++ b/src/layout/Summary2/CommonSummaryComponents/MultipleValueSummary.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { ErrorMessage, Label, List, Paragraph } from '@digdir/designsystemet-react'; import cn from 'classnames'; +import dot from 'dot-object'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; @@ -11,12 +12,14 @@ import { useUnifiedValidationsForNode } from 'src/features/validation/selectors/ import { validationsOfSeverity } from 'src/features/validation/utils'; import { EditButton } from 'src/layout/Summary2/CommonSummaryComponents/EditButton'; import classes from 'src/layout/Summary2/CommonSummaryComponents/MultipleValueSummary.module.css'; -import { useNodeFormData } from 'src/utils/layout/useNodeItem'; +import { useNodeFormData, useNodeItem } from 'src/utils/layout/useNodeItem'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; type ValidTypes = 'MultipleSelect' | 'Checkboxes'; type ValidNodes = LayoutNode; +type Row = Record; + interface MultipleValueSummaryProps { title: React.ReactNode; componentNode: ValidNodes; @@ -46,16 +49,33 @@ export const MultipleValueSummary = ({ isCompact, emptyFieldText, }: MultipleValueSummaryProps) => { + const { dataModelBindings } = useNodeItem(componentNode); const options = useNodeOptions(componentNode).options; const rawFormData = useNodeFormData(componentNode); const { langAsString } = useLanguage(); - const displayValues = Object.values(getCommaSeparatedOptionsToText(rawFormData.simpleBinding, options, langAsString)); + + const relativeCheckedPath = + dataModelBindings?.checked && dataModelBindings?.group + ? dataModelBindings.checked.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const relativeSimpleBindingPath = + dataModelBindings?.simpleBinding && dataModelBindings?.group + ? dataModelBindings.simpleBinding.field.replace(`${dataModelBindings.group.field}.`, '') + : undefined; + + const displayRows: string[] = (rawFormData?.group as unknown as Row[]) + ?.filter((row) => (!relativeCheckedPath ? true : dot.pick(relativeCheckedPath, row) === true)) + .map((row) => (!relativeSimpleBindingPath ? true : dot.pick(relativeSimpleBindingPath, row))); + + const displayValues = dataModelBindings.group + ? displayRows + : Object.values(getCommaSeparatedOptionsToText(rawFormData.simpleBinding, options, langAsString)); const validations = useUnifiedValidationsForNode(componentNode); const errors = validationsOfSeverity(validations, 'error'); const displayType = getDisplayType(displayValues, showAsList, isCompact); - return (
From 50dc8b0581ca61623a4afae6fdefbff82e60998c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Tue, 6 May 2025 10:05:02 +0200 Subject: [PATCH 58/59] Fix error on tests with useDisplayData on Checkboxes and Multiselect --- src/layout/Checkboxes/index.tsx | 10 ++++++---- src/layout/MultipleSelect/index.tsx | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/layout/Checkboxes/index.tsx b/src/layout/Checkboxes/index.tsx index f2fd5835ac..20dc983bd8 100644 --- a/src/layout/Checkboxes/index.tsx +++ b/src/layout/Checkboxes/index.tsx @@ -13,8 +13,8 @@ import { CheckboxContainerComponent } from 'src/layout/Checkboxes/CheckboxesCont import { CheckboxesSummary } from 'src/layout/Checkboxes/CheckboxesSummary'; import { CheckboxesDef } from 'src/layout/Checkboxes/config.def.generated'; import { MultipleChoiceSummary } from 'src/layout/Checkboxes/MultipleChoiceSummary'; -import { useNode } from 'src/utils/layout/NodesContext'; -import { useNodeFormDataWhenType, useNodeItem } from 'src/utils/layout/useNodeItem'; +import { NodesInternal, useNode } from 'src/utils/layout/NodesContext'; +import { useNodeFormDataWhenType } from 'src/utils/layout/useNodeItem'; import type { LayoutValidationCtx } from 'src/features/devtools/layoutValidation/types'; import type { ComponentValidation } from 'src/features/validation'; import type { PropsFromGenericComponent } from 'src/layout'; @@ -42,8 +42,10 @@ export class Checkboxes extends CheckboxesDef { if (!node) { return ''; } - - const dataModelBindings = useNodeItem(node as LayoutNode<'Checkboxes'>, (i) => i.dataModelBindings); + const dataModelBindings = NodesInternal.useNodeData( + node as LayoutNode<'Checkboxes'>, + (data) => data.layout.dataModelBindings, + ); const relativeCheckedPath = dataModelBindings?.checked && dataModelBindings?.group diff --git a/src/layout/MultipleSelect/index.tsx b/src/layout/MultipleSelect/index.tsx index c6b0e8ea12..dc32594508 100644 --- a/src/layout/MultipleSelect/index.tsx +++ b/src/layout/MultipleSelect/index.tsx @@ -13,8 +13,8 @@ import { MultipleChoiceSummary } from 'src/layout/Checkboxes/MultipleChoiceSumma import { MultipleSelectDef } from 'src/layout/MultipleSelect/config.def.generated'; import { MultipleSelectComponent } from 'src/layout/MultipleSelect/MultipleSelectComponent'; import { MultipleSelectSummary } from 'src/layout/MultipleSelect/MultipleSelectSummary'; -import { useNode } from 'src/utils/layout/NodesContext'; -import { useNodeFormDataWhenType, useNodeItem } from 'src/utils/layout/useNodeItem'; +import { NodesInternal, useNode } from 'src/utils/layout/NodesContext'; +import { useNodeFormDataWhenType } from 'src/utils/layout/useNodeItem'; import type { LayoutValidationCtx } from 'src/features/devtools/layoutValidation/types'; import type { ComponentValidation } from 'src/features/validation'; import type { PropsFromGenericComponent } from 'src/layout'; @@ -41,8 +41,10 @@ export class MultipleSelect extends MultipleSelectDef { if (!node) { return ''; } - - const dataModelBindings = useNodeItem(node as LayoutNode<'MultipleSelect'>, (i) => i.dataModelBindings); + const dataModelBindings = NodesInternal.useNodeData( + node as LayoutNode<'MultipleSelect'>, + (data) => data.layout.dataModelBindings, + ); const relativeCheckedPath = dataModelBindings?.checked && dataModelBindings?.group From 53721a6dd7e91f3a6a1d2bbd50f8dd9de887da4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Wed, 7 May 2025 09:42:21 +0200 Subject: [PATCH 59/59] Add test for summary2 when using group and soft deletion --- .../component-library/checkboxes.ts | 39 ++++++++++++++++++ .../component-library/multiple-select.ts | 41 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/test/e2e/integration/component-library/checkboxes.ts b/test/e2e/integration/component-library/checkboxes.ts index d199e24562..d514bd03cd 100644 --- a/test/e2e/integration/component-library/checkboxes.ts +++ b/test/e2e/integration/component-library/checkboxes.ts @@ -96,4 +96,43 @@ describe('Checkboxes component', () => { cy.get(repGroup).findByRole('cell', { name: checkboxText2 }).should('exist'); cy.get(repGroup).findAllByRole('cell', { name: '20' }).should('exist'); }); + + it('Renders the summary2 component with correct text for Checkboxes with group and soft deletion', () => { + cy.startAppInstance(appFrontend.apps.componentLibrary, { authenticationLevel: '2' }); + cy.gotoNavPage('Avkryssningsbokser'); + + const checkboxes = '[data-componentid=CheckboxesPage-Checkboxes2]'; + const summary2 = '[data-componentid=CheckboxesPage-Header-Summary2-Component2]'; + + const checkboxText1 = 'Karoline'; + const checkboxText2 = 'Kåre'; + const checkboxText3 = 'Johanne'; + const checkboxText4 = 'Kari'; + const checkboxText5 = 'Petter'; + + //Check options in checkboxes component + cy.get(checkboxes).contains('label', checkboxText1).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText2).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText3).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText4).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText5).prev('input[type="checkbox"]').click(); + + //Uncheck + cy.get(checkboxes).contains('label', checkboxText4).prev('input[type="checkbox"]').click(); + cy.get(checkboxes).contains('label', checkboxText5).prev('input[type="checkbox"]').click(); + + //Check that checkboxes is correct + cy.get(checkboxes).contains('label', checkboxText1).prev('input[type="checkbox"]').should('be.checked'); + cy.get(checkboxes).contains('label', checkboxText2).prev('input[type="checkbox"]').should('be.checked'); + cy.get(checkboxes).contains('label', checkboxText3).prev('input[type="checkbox"]').should('be.checked'); + cy.get(checkboxes).contains('label', checkboxText4).prev('input[type="checkbox"]').should('not.be.checked'); + cy.get(checkboxes).contains('label', checkboxText5).prev('input[type="checkbox"]').should('not.be.checked'); + + const expectedText = [checkboxText1, checkboxText2, checkboxText3].join(', '); + + cy.get(`div${summary2}`) + .next() + .find('span.fds-paragraph') // Targets the span with the summary text + .should('have.text', expectedText); + }); }); diff --git a/test/e2e/integration/component-library/multiple-select.ts b/test/e2e/integration/component-library/multiple-select.ts index d03e11dfcb..c1ae1417ec 100644 --- a/test/e2e/integration/component-library/multiple-select.ts +++ b/test/e2e/integration/component-library/multiple-select.ts @@ -98,4 +98,45 @@ describe('Multiple select component', () => { cy.get(repGroup).findByRole('cell', { name: checkboxText2 }).should('exist'); cy.get(repGroup).findAllByRole('cell', { name: '20' }).should('exist'); }); + it('Renders the summary2 component with correct text for MultipleSelext with group and soft deletion', () => { + cy.startAppInstance(appFrontend.apps.componentLibrary, { authenticationLevel: '2' }); + cy.gotoNavPage('Flervalg'); + + const multiselect = '#form-content-MultipleSelectPage-Checkboxes2'; + const multiselectList = 'div[role="listbox"]'; + const summary2 = '[data-componentid=MultipleSelectPage-Header-Summary2-Component2]'; + + const checkboxText1 = 'Karoline'; + const checkboxText2 = 'Kåre'; + const checkboxText3 = 'Johanne'; + const checkboxText4 = 'Kari'; + const checkboxText5 = 'Petter'; + + cy.get(multiselect).click(); + + //Check options in checkboxes component + cy.get(multiselectList).contains('span', checkboxText1).click(); + cy.get(multiselectList).contains('span', checkboxText2).click(); + cy.get(multiselectList).contains('span', checkboxText3).click(); + cy.get(multiselectList).contains('span', checkboxText4).click(); + cy.get(multiselectList).contains('span', checkboxText5).click(); + + //Uncheck + cy.get(multiselectList).contains('span', checkboxText4).click(); + cy.get(multiselectList).contains('span', checkboxText5).click(); + + //Check that checkboxes is correct + cy.get(multiselect).contains('span', checkboxText1).should('exist'); + cy.get(multiselect).contains('span', checkboxText2).should('exist'); + cy.get(multiselect).contains('span', checkboxText3).should('exist'); + cy.get(multiselect).contains('span', checkboxText4).should('not.exist'); + cy.get(multiselect).contains('span', checkboxText5).should('not.exist'); + + const expectedText = [checkboxText1, checkboxText2, checkboxText3].join(', '); + + cy.get(`div${summary2}`) + .next() + .find('span.fds-paragraph') // Targets the span with the summary text + .should('have.text', expectedText); + }); });