From 26909acb54879552fd8ad0d713d2284a0d658724 Mon Sep 17 00:00:00 2001 From: Magnus Revheim Martinsen Date: Thu, 14 Nov 2024 08:46:11 +0100 Subject: [PATCH] Repeating group summary2 table mode (#2712) * add table view of repeating group summary * support text, date and number in repeating group * fix expression tests * show data, text and number by default if included in repeating group * add mobile view * remove unused const * fix edit button * add test for rep group table summary --- .../functions/displayValue/date.json | 32 ++++ .../functions/displayValue/number.json | 31 ++++ .../functions/displayValue/text.json | 31 ++++ src/layout/Date/index.tsx | 21 ++- src/layout/Number/index.tsx | 6 + .../Summary2/RepeatingGroupSummary.tsx | 7 + .../RepeatingGroupTableSummary.module.css | 78 +++++++++ .../RepeatingGroupTableSummary.tsx | 151 ++++++++++++++++++ .../Table/RepeatingGroupTableTitle.tsx | 2 +- src/layout/RepeatingGroup/config.ts | 10 +- src/layout/RepeatingGroup/index.tsx | 2 + src/layout/RepeatingGroup/useTableNodes.ts | 4 +- .../CommonSummaryComponents/EditButton.tsx | 2 +- src/layout/Text/index.tsx | 6 + .../component-library/repeating-group.ts | 25 +++ 15 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 src/features/expressions/shared-tests/functions/displayValue/date.json create mode 100644 src/features/expressions/shared-tests/functions/displayValue/number.json create mode 100644 src/features/expressions/shared-tests/functions/displayValue/text.json create mode 100644 src/layout/RepeatingGroup/Summary2/RepeatingGroupTableSummary/RepeatingGroupTableSummary.module.css create mode 100644 src/layout/RepeatingGroup/Summary2/RepeatingGroupTableSummary/RepeatingGroupTableSummary.tsx diff --git a/src/features/expressions/shared-tests/functions/displayValue/date.json b/src/features/expressions/shared-tests/functions/displayValue/date.json new file mode 100644 index 0000000000..194fd0d1f6 --- /dev/null +++ b/src/features/expressions/shared-tests/functions/displayValue/date.json @@ -0,0 +1,32 @@ +{ + "name": "Display value of Date component", + "expression": [ + "displayValue", + "dag" + ], + "context": { + "component": "dag", + "currentLayout": "Page" + }, + "expects": "21.07.2023", + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "dag", + "type": "Date", + "value": ["dataModel", "Skjema.Dag"], + "format": "dd.MM.yyyy" + } + ] + } + } + }, + "dataModel": { + "Skjema": { + "Dag": "2023-07-21" + } + } +} diff --git a/src/features/expressions/shared-tests/functions/displayValue/number.json b/src/features/expressions/shared-tests/functions/displayValue/number.json new file mode 100644 index 0000000000..ffa92bb945 --- /dev/null +++ b/src/features/expressions/shared-tests/functions/displayValue/number.json @@ -0,0 +1,31 @@ +{ + "name": "Display value of Number component", + "expression": [ + "displayValue", + "penger" + ], + "context": { + "component": "penger", + "currentLayout": "Page" + }, + "expects": "1200", + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "penger", + "type": "Number", + "value": ["dataModel", "Skjema.Penger"] + } + ] + } + } + }, + "dataModel": { + "Skjema": { + "Penger": "1200" + } + } +} diff --git a/src/features/expressions/shared-tests/functions/displayValue/text.json b/src/features/expressions/shared-tests/functions/displayValue/text.json new file mode 100644 index 0000000000..12d02a86e7 --- /dev/null +++ b/src/features/expressions/shared-tests/functions/displayValue/text.json @@ -0,0 +1,31 @@ +{ + "name": "Display value of Text component", + "expression": [ + "displayValue", + "navn" + ], + "context": { + "component": "navn", + "currentLayout": "Page" + }, + "expects": "Per", + "layouts": { + "Page": { + "$schema": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "data": { + "layout": [ + { + "id": "navn", + "type": "Text", + "value": ["dataModel", "Skjema.Navn"] + } + ] + } + } + }, + "dataModel": { + "Skjema": { + "Navn": "Per" + } + } +} diff --git a/src/layout/Date/index.tsx b/src/layout/Date/index.tsx index b2bfdf66e3..f66f9f6231 100644 --- a/src/layout/Date/index.tsx +++ b/src/layout/Date/index.tsx @@ -1,8 +1,9 @@ import React, { forwardRef } from 'react'; import type { JSX } from 'react'; -import { formatDate, isValid } from 'date-fns'; +import { formatDate, isValid, parseISO } from 'date-fns'; +import { useDisplayDataProps } from 'src/features/displayData/useDisplayData'; import { DateDef } from 'src/layout/Date/config.def.generated'; import { DateComponent } from 'src/layout/Date/DateComponent'; import type { DisplayDataProps } from 'src/features/displayData'; @@ -14,11 +15,25 @@ export class Date extends DateDef { getDisplayData(node: LayoutNode<'Date'>, { nodeDataSelector }: DisplayDataProps): string { const dateString = nodeDataSelector((picker) => picker(node)?.item?.value, [node]); const format = nodeDataSelector((picker) => picker(node)?.item?.format, [node]); - if (dateString === undefined || !isValid(dateString)) { + + if (dateString === undefined) { return ''; } - return formatDate(dateString, format || 'dd.MM.yyyy'); + const parsedValue = parseISO(dateString); + let displayData = parsedValue.toDateString(); + if (!isValid(parsedValue)) { + displayData = 'Ugyldig format'; + } else if (format) { + displayData = formatDate(parsedValue, format || 'dd.MM.yyyy'); + } + + return displayData; + } + + useDisplayData(node: LayoutNode<'Date'>): string { + const displayDataProps = useDisplayDataProps(); + return this.getDisplayData(node, displayDataProps); } render = forwardRef>( diff --git a/src/layout/Number/index.tsx b/src/layout/Number/index.tsx index b54b998933..dad9f5c00d 100644 --- a/src/layout/Number/index.tsx +++ b/src/layout/Number/index.tsx @@ -3,6 +3,7 @@ import type { JSX } from 'react'; import { formatNumericText } from '@digdir/design-system-react'; +import { useDisplayDataProps } from 'src/features/displayData/useDisplayData'; import { getMapToReactNumberConfig } from 'src/hooks/useMapToReactNumberConfig'; import { evalFormatting } from 'src/layout/Input/formatting'; import { NumberDef } from 'src/layout/Number/config.def.generated'; @@ -30,6 +31,11 @@ export class Number extends NumberDef { return text; } + useDisplayData(node: LayoutNode<'Number'>): string { + const displayDataProps = useDisplayDataProps(); + return this.getDisplayData(node, displayDataProps); + } + render = forwardRef>( function LayoutComponentNumberRender(props, _): JSX.Element | null { return ; diff --git a/src/layout/RepeatingGroup/Summary2/RepeatingGroupSummary.tsx b/src/layout/RepeatingGroup/Summary2/RepeatingGroupSummary.tsx index 521fb5f10a..5a7e118560 100644 --- a/src/layout/RepeatingGroup/Summary2/RepeatingGroupSummary.tsx +++ b/src/layout/RepeatingGroup/Summary2/RepeatingGroupSummary.tsx @@ -10,6 +10,7 @@ import { useUnifiedValidationsForNode } from 'src/features/validation/selectors/ import { validationsOfSeverity } from 'src/features/validation/utils'; import { useRepeatingGroupRowState } from 'src/layout/RepeatingGroup/Providers/RepeatingGroupContext'; import classes from 'src/layout/RepeatingGroup/Summary2/RepeatingGroupSummary.module.css'; +import { RepeatingGroupTableSummary } from 'src/layout/RepeatingGroup/Summary2/RepeatingGroupTableSummary/RepeatingGroupTableSummary'; import { SingleValueSummary } from 'src/layout/Summary2/CommonSummaryComponents/SingleValueSummary'; import { ComponentSummary } from 'src/layout/Summary2/SummaryComponent2/ComponentSummary'; import { BaseLayoutNode } from 'src/utils/layout/LayoutNode'; @@ -18,10 +19,12 @@ import { useNodeItem } from 'src/utils/layout/useNodeItem'; export const RepeatingGroupSummary = ({ componentNode, isCompact, + display, emptyFieldText, }: { componentNode: BaseLayoutNode<'RepeatingGroup'>; isCompact?: boolean; + display?: 'table' | 'full'; emptyFieldText?: string; }) => { const { visibleRows } = useRepeatingGroupRowState(); @@ -44,6 +47,10 @@ export const RepeatingGroupSummary = ({ ); } + if (display === 'table' && componentNode) { + return ; + } + return (
; + isCompact?: boolean; + emptyFieldText?: string; +}) => { + const isMobile = useIsMobile(); + const pdfModeActive = usePdfModeActive(); + const isSmall = isMobile && !pdfModeActive; + const { visibleRows } = useRepeatingGroupRowState(); + const rowsToDisplaySet = new Set(visibleRows.map((row) => row.uuid)); + const rows = useNodeItem(componentNode, (i) => i.rows).filter((row) => row && rowsToDisplaySet.has(row.uuid)); + const validations = useUnifiedValidationsForNode(componentNode); + const errors = validationsOfSeverity(validations, 'error'); + const title = useNodeItem(componentNode, (i) => i.textResourceBindings?.title); + const childNodes = useTableNodes(componentNode, 0); + const { tableColumns } = useNodeItem(componentNode); + const columnSettings = tableColumns ? structuredClone(tableColumns) : ({} as ITableColumnFormatting); + + if (rows.length === 0) { + return ( + + ); + } + + return ( +
+ +
} /> + + + {childNodes.map((childNode) => ( + + ))} + {!pdfModeActive && !isSmall && ( + + + + + + )} + + + + {rows.map((row) => ( + + {childNodes.map((node) => ( + + ))} + {!pdfModeActive && ( + + {row?.items && row?.items?.length > 0 && } + + )} + + ))} + +
+ {errors?.map(({ message }) => ( + + + + + ))} +
+ ); +}; + +function HeaderCell({ node, columnSettings }: { node: LayoutNode; columnSettings: ITableColumnFormatting }) { + const style = useColumnStylesRepeatingGroups(node, columnSettings); + return ( + + + + ); +} + +function DataCell({ node }) { + const { langAsString } = useLanguage(); + const displayDataProps = useDisplayDataProps(); + const headerTitle = langAsString(useTableTitle(node)); + return ( + + {'getDisplayData' in node.def && node.def.getDisplayData(node as never, displayDataProps)} + + ); +} diff --git a/src/layout/RepeatingGroup/Table/RepeatingGroupTableTitle.tsx b/src/layout/RepeatingGroup/Table/RepeatingGroupTableTitle.tsx index 67cedc5c71..96ac0e5917 100644 --- a/src/layout/RepeatingGroup/Table/RepeatingGroupTableTitle.tsx +++ b/src/layout/RepeatingGroup/Table/RepeatingGroupTableTitle.tsx @@ -25,7 +25,7 @@ export const RepeatingGroupTableTitle = ({ node, columnSettings }: IProps) => { ); }; -function useTableTitle(node: LayoutNode) { +export function useTableTitle(node: LayoutNode) { const textResourceBindings = useNodeItem(node, (i) => i.textResourceBindings); if (!textResourceBindings) { diff --git a/src/layout/RepeatingGroup/config.ts b/src/layout/RepeatingGroup/config.ts index 83d396d98a..a2ad34c43c 100644 --- a/src/layout/RepeatingGroup/config.ts +++ b/src/layout/RepeatingGroup/config.ts @@ -4,7 +4,15 @@ import { CompCategory } from 'src/layout/common'; import { GridRowsPlugin } from 'src/layout/Grid/GridRowsPlugin'; import { RepeatingChildrenPlugin } from 'src/utils/layout/plugins/RepeatingChildrenPlugin'; -export const REPEATING_GROUP_SUMMARY_OVERRIDE_PROPS = new CG.obj() +export const REPEATING_GROUP_SUMMARY_OVERRIDE_PROPS = new CG.obj( + new CG.prop( + 'display', + new CG.enum('table', 'full') + .optional({ default: 'full' }) + .setTitle('Display type') + .setDescription('Show the summary as a table or as full summary components'), + ), +) .extends(CG.common('ISummaryOverridesCommon')) .optional() .setTitle('Summary properties') diff --git a/src/layout/RepeatingGroup/index.tsx b/src/layout/RepeatingGroup/index.tsx index 819de7c835..f17fa831db 100644 --- a/src/layout/RepeatingGroup/index.tsx +++ b/src/layout/RepeatingGroup/index.tsx @@ -14,6 +14,7 @@ import type { LayoutValidationCtx } from 'src/features/devtools/layoutValidation import type { BaseValidation, ComponentValidation, ValidationDataSources } from 'src/features/validation'; import type { ExprResolver, SummaryRendererProps } from 'src/layout/LayoutComponent'; import type { GroupExpressions, RepGroupInternal, RepGroupRowExtras } from 'src/layout/RepeatingGroup/types'; +import type { RepeatingGroupSummaryOverrideProps } from 'src/layout/Summary2/config.generated'; import type { Summary2Props } from 'src/layout/Summary2/SummaryComponent2/types'; import type { LayoutNode } from 'src/utils/layout/LayoutNode'; import type { NodeDataSelector } from 'src/utils/layout/NodesContext'; @@ -84,6 +85,7 @@ export class RepeatingGroup extends RepeatingGroupDef implements ValidateCompone componentNode={props.target} isCompact={props.isCompact} emptyFieldText={props.override?.emptyFieldText} + display={(props.override as RepeatingGroupSummaryOverrideProps)?.display} /> ); diff --git a/src/layout/RepeatingGroup/useTableNodes.ts b/src/layout/RepeatingGroup/useTableNodes.ts index 7d7bdff363..8f01b4d50c 100644 --- a/src/layout/RepeatingGroup/useTableNodes.ts +++ b/src/layout/RepeatingGroup/useTableNodes.ts @@ -11,7 +11,9 @@ export function useTableNodes(node: LayoutNode<'RepeatingGroup'>, restriction: T return useMemo(() => { const nodes = children.filter((child) => - tableHeaders ? tableHeaders.includes(child.baseId) : child.isCategory(CompCategory.Form), + tableHeaders + ? tableHeaders.includes(child.baseId) + : child.isCategory(CompCategory.Form) || child.isType('Text') || child.isType('Number') || child.isType('Date'), ); // Sort using the order from tableHeaders diff --git a/src/layout/Summary2/CommonSummaryComponents/EditButton.tsx b/src/layout/Summary2/CommonSummaryComponents/EditButton.tsx index a6f60aaf85..807a7fffa1 100644 --- a/src/layout/Summary2/CommonSummaryComponents/EditButton.tsx +++ b/src/layout/Summary2/CommonSummaryComponents/EditButton.tsx @@ -17,7 +17,7 @@ import type { LayoutNode } from 'src/utils/layout/LayoutNode'; type EditButtonProps = { componentNode: LayoutNode; - summaryComponentId: string; + summaryComponentId?: string; navigationOverride?: (() => Promise | void) | null; } & React.HTMLAttributes; diff --git a/src/layout/Text/index.tsx b/src/layout/Text/index.tsx index e5582ba168..b76fb3d3a9 100644 --- a/src/layout/Text/index.tsx +++ b/src/layout/Text/index.tsx @@ -1,6 +1,7 @@ import React, { forwardRef } from 'react'; import type { JSX } from 'react'; +import { useDisplayDataProps } from 'src/features/displayData/useDisplayData'; import { TextDef } from 'src/layout/Text/config.def.generated'; import { TextComponent } from 'src/layout/Text/TextComponent'; import type { DisplayDataProps } from 'src/features/displayData'; @@ -17,6 +18,11 @@ export class Text extends TextDef { return text; } + useDisplayData(node: LayoutNode<'Text'>): string { + const displayDataProps = useDisplayDataProps(); + return this.getDisplayData(node, displayDataProps); + } + render = forwardRef>( function LayoutComponentTextRender(props, _): JSX.Element | null { return ; diff --git a/test/e2e/integration/component-library/repeating-group.ts b/test/e2e/integration/component-library/repeating-group.ts index d6a952e5ac..d21f1c451d 100644 --- a/test/e2e/integration/component-library/repeating-group.ts +++ b/test/e2e/integration/component-library/repeating-group.ts @@ -23,6 +23,31 @@ describe('Group summary test', () => { }); }); + it('Displays a summary for a filled repeating group in table', () => { + const inputValue = 'Test input for group'; + + cy.findAllByRole('button', { name: /Legg til ny/ }) + .first() + .click(); + cy.findByRole('textbox', { name: /Navn/ }).type(inputValue); + cy.findAllByRole('button', { name: /Lagre og lukk/ }) + .first() + .click(); + cy.findAllByRole('button', { name: /Legg til ny/ }) + .first() + .click(); + cy.findByRole('textbox', { name: /Navn/ }).type(inputValue); + cy.findAllByRole('button', { name: /Lagre og lukk/ }) + .first() + .click(); + + cy.get('div[data-testid="summary-repeating-group-component"] > table').within(() => { + cy.findAllByRole('row').should('have.length', 3); + cy.findByRole('columnheader', { name: /Navn/ }).should('exist'); + cy.findAllByRole('cell', { name: inputValue }).first().should('exist'); + }); + }); + it('Fills in an input in the nested repeating group, the text appears in summary', () => { const inputValue = 'Test input inside nested repeating group';