From 12490dff5cd2a65740bf8f2ae7071672833efc1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Myran?= Date: Wed, 17 Dec 2025 10:22:52 +0100 Subject: [PATCH] Added column configuration on grid, support hidden property which supports expression and validate if config is overlapping or differs. --- src/codegen/Common.ts | 23 +++++++++++++++ src/language/texts/en.ts | 4 +++ src/language/texts/nb.ts | 5 ++++ src/language/texts/nn.ts | 4 +++ src/layout/Grid/GridComponent.tsx | 41 +++++++++++++++++++++----- src/layout/Grid/GridLayoutValidator.ts | 39 ++++++++++++++++++++++++ src/layout/Grid/config.ts | 1 + src/layout/Grid/index.tsx | 24 +++++++++++++-- src/layout/RepeatingGroup/config.ts | 2 +- 9 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 src/layout/Grid/GridLayoutValidator.ts diff --git a/src/codegen/Common.ts b/src/codegen/Common.ts index a99d8b6fe5..c59823787b 100644 --- a/src/codegen/Common.ts +++ b/src/codegen/Common.ts @@ -446,6 +446,29 @@ const common = { ), ITableColumnFormatting: () => new CG.obj().additionalProperties(CG.common('ITableColumnProperties')), ITableColumnProperties: () => + new CG.obj( + new CG.prop( + 'width', + new CG.str() + .optional({ default: 'auto' }) + .setTitle('Width') + .setDescription("Width of cell in % or 'auto'. Defaults to 'auto'") + .setPattern(/^([0-9]{1,2}%|100%|auto)$/), + ), + new CG.prop('alignText', CG.common('ITableColumnsAlignText').optional()), + new CG.prop('textOverflow', CG.common('ITableColumnsTextOverflow').optional()), + ) + .setTitle('Column options') + .setDescription('Options for the row/column') + .addExample({ + width: 'auto', + alignText: 'left', + textOverflow: { + lineWrap: true, + maxHeight: 2, + }, + }), + ITableColumnPropertiesWithHidden: () => new CG.obj( new CG.prop( 'width', diff --git a/src/language/texts/en.ts b/src/language/texts/en.ts index c0ede743d4..60b4aee8ad 100644 --- a/src/language/texts/en.ts +++ b/src/language/texts/en.ts @@ -415,6 +415,10 @@ export function en() { 'config_error.group_no_deletion_strategy': 'When you have set group, you must also set deletionStrategy.', 'config_error.soft_delete_no_checked': 'When you have set deletionStrategy to soft, you must also set "checked".', 'config_error.hard_delete_with_checked': 'When you have set deletionStrategy to hard, you cannot set "checked".', + 'config_error.grid_diff_cell_columns': + 'There is a difference in number of cells({0}) on row {1} and the number of columns({2}). Make sure the rows have correct number of cells({2}).', + 'config_error.grid_column_option_cell': + 'You have a columnOption configuration on both cell and columns property. Please use the column property instead.', 'version_error.version_mismatch': 'Version mismatch', 'version_error.version_mismatch_message': 'This version of the app frontend is not compatible with the version of the backend libraries you are using. Update to the latest version of the packages and try again.', diff --git a/src/language/texts/nb.ts b/src/language/texts/nb.ts index acdb8b2ba2..a6b45c1d33 100644 --- a/src/language/texts/nb.ts +++ b/src/language/texts/nb.ts @@ -419,6 +419,11 @@ export function nb() { 'config_error.group_no_deletion_strategy': 'Når du har satt group må du også sette deletionStrategy.', 'config_error.soft_delete_no_checked': 'Når du har satt deletionStrategy til soft må du også sette "checked".', 'config_error.hard_delete_with_checked': 'Når du har satt deletionStrategy til hard kan du ikke sette "checked".', + 'config_error.grid_diff_cell_columns': + 'Det er forskjell i antall celler({0}) på rad {1} og antall kolonner({2}). Sørg for at alle rader har korrekt antall celler({2}).', + 'config_error.grid_column_option_cell': + '"columnOption"-konfigurasjon er spesifisert både på "cell" og på "columns" egenskapene. Vennligst bruk "columns" egenskapen.', + 'version_error.version_mismatch': 'Versjonsfeil', 'version_error.version_mismatch_message': 'Denne versjonen av app frontend er ikke kompatibel med den versjonen av backend-bibliotekene du bruker. Oppdater til nyeste versjon av pakkene og prøv igjen.', diff --git a/src/language/texts/nn.ts b/src/language/texts/nn.ts index d8f2f4e063..626cff3a81 100644 --- a/src/language/texts/nn.ts +++ b/src/language/texts/nn.ts @@ -416,6 +416,10 @@ export function nn() { 'config_error.group_no_deletion_strategy': 'Når du har sett group, må du også setje deletionStrategy.', 'config_error.soft_delete_no_checked': 'Når du har sett deletionStrategy til soft, må du også setje checked.', 'config_error.hard_delete_with_checked': 'Når du har sett deletionStrategy til hard, kan du ikkje setje checked.', + 'config_error.grid_diff_cell_columns': + 'Det er forskjell i talet på celler({0}) på rad {1} og talet på kolonnar({2}). Sørg for at alle rader har korrekt tal på celler({2}).', + 'config_error.grid_column_option_cell': + '"columnOption"-konfigurasjon er spesifisert både på "cell" og på "columns" eigenskapane. Ver vennleg og bruk "columns" eigenskapen.', 'version_error.version_mismatch': 'Versjonsfeil', 'version_error.version_mismatch_message': 'Denne versjonen av app frontend er ikkje kompatibel med den versjonen av backend-biblioteka du brukar. Oppdater til nyaste versjon av pakkane og prøv igjen.', diff --git a/src/layout/Grid/GridComponent.tsx b/src/layout/Grid/GridComponent.tsx index 9569cbd3dc..7502a3e12f 100644 --- a/src/layout/Grid/GridComponent.tsx +++ b/src/layout/Grid/GridComponent.tsx @@ -29,11 +29,17 @@ import { useIsHidden } from 'src/utils/layout/hidden'; import { useLabel } from 'src/utils/layout/useLabel'; import { useItemFor, useItemWhenType } from 'src/utils/layout/useNodeItem'; import type { PropsFromGenericComponent } from 'src/layout'; -import type { GridCell, GridRow, ITableColumnFormatting, ITableColumnProperties } from 'src/layout/common.generated'; +import type { + GridCell, + GridRow, + ITableColumnFormatting, + ITableColumnProperties, + ITableColumnPropertiesWithHidden, +} from 'src/layout/common.generated'; export function RenderGrid(props: PropsFromGenericComponent<'Grid'>) { const { baseComponentId } = props; - const { rows, textResourceBindings, labelSettings } = useItemWhenType(baseComponentId, 'Grid'); + const { rows, textResourceBindings, labelSettings, columns } = useItemWhenType(baseComponentId, 'Grid'); const { title, description, help } = textResourceBindings ?? {}; const columnSettings: ITableColumnFormatting = {}; const isMobile = useIsMobile(); @@ -68,6 +74,7 @@ export function RenderGrid(props: PropsFromGenericComponent<'Grid'>) { )} @@ -78,12 +85,13 @@ export function RenderGrid(props: PropsFromGenericComponent<'Grid'>) { interface GridRowsProps { rows: GridRow[]; + columns?: ITableColumnProperties[] | undefined; extraCells?: GridCell[]; isNested: boolean; mutableColumnSettings: ITableColumnFormatting; } -export function GridRowsRenderer({ rows, extraCells = [], isNested, mutableColumnSettings }: GridRowsProps) { +export function GridRowsRenderer({ rows, columns, extraCells = [], isNested, mutableColumnSettings }: GridRowsProps) { const batches: { type: 'header' | 'body'; rows: GridRow[] }[] = []; for (const row of rows) { @@ -106,6 +114,7 @@ export function GridRowsRenderer({ rows, extraCells = [], isNested, mutableColum {batch.rows.map((row, rowIdx) => ( { row: GridRow; + columns?: ITableColumnProperties[] | undefined; } -function GridRowRenderer({ row, isNested, mutableColumnSettings }: GridRowProps) { +function GridRowRenderer({ row, columns, isNested, mutableColumnSettings }: GridRowProps) { const rowHidden = useIsGridRowHidden(row); if (rowHidden) { return null; @@ -137,14 +147,17 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings }: GridRowProps) [css.fullWidthCellFirst]: isFirst && !isNested, [css.fullWidthCellLast]: isLast && !isNested, }); - if (row.header && cell && 'columnOptions' in cell && cell.columnOptions) { // eslint-disable-next-line react-compiler/react-compiler mutableColumnSettings[cellIdx] = cell.columnOptions; } + if (cell && columns && columns[cellIdx]) { + mutableColumnSettings[cellIdx] = { ...mutableColumnSettings[cellIdx], ...columns[cellIdx] }; //TODO: På sikt fjerne muligheten til å konfigurere på celle nivå. + } + if (isGridCellText(cell) || isGridCellLabelFrom(cell)) { - let textCellSettings: ITableColumnProperties = mutableColumnSettings[cellIdx] + let textCellSettings: ITableColumnPropertiesWithHidden = mutableColumnSettings[cellIdx] ? structuredClone(mutableColumnSettings[cellIdx]) : {}; textCellSettings = { ...textCellSettings, ...cell }; @@ -202,7 +215,7 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings }: GridRowProps) interface CellProps { className?: string; - columnStyleOptions?: ITableColumnProperties; + columnStyleOptions?: ITableColumnPropertiesWithHidden; isHeader?: boolean; rowReadOnly?: boolean; } @@ -229,6 +242,10 @@ function CellWithComponent({ const isHidden = useIsHidden(baseComponentId); const CellComponent = isHeader ? Table.HeaderCell : Table.Cell; + if (columnStyleOptions?.hidden === true) { + return; + } + if (!isHidden) { const columnStyles = columnStyleOptions && getColumnStyles(columnStyleOptions); return ( @@ -257,6 +274,10 @@ function CellWithText({ children, className, columnStyleOptions, help, isHeader const { elementAsString } = useLanguage(); const CellComponent = isHeader ? Table.HeaderCell : Table.Cell; + if (columnStyleOptions?.hidden === true) { + return; + } + return ( ): JSX.Element | null { + const { intermediateItem, externalItem } = props; + const rows = externalItem?.rows; + const columns = externalItem?.columns; + + const { langAsString } = useLanguage(); + const addError = NodesInternal.useAddError(); + + useEffect(() => { + let error: string | null = null; + + if (Array.isArray(rows)) { + for (const [i, row] of rows.entries()) { + if (Array.isArray(row?.cells) && Array.isArray(columns) && row.cells.length !== columns?.length) { + error = langAsString(`config_error.grid_diff_cell_columns`, [row.cells.length, i + 1, columns?.length]); + } + for (const [_, cell] of row.cells.entries()) { + if (cell?.columnOptions && columns) { + error = langAsString('config_error.grid_column_option_cell'); + } + } + } + } + + if (error) { + addError(error, intermediateItem.id, 'node'); + window.logErrorOnce(`Validation error for '${intermediateItem.id}': ${error}`); + } + }, [addError, columns, columns?.length, intermediateItem.id, langAsString, rows]); + + return null; +} diff --git a/src/layout/Grid/config.ts b/src/layout/Grid/config.ts index f9b82ff289..bbf2f60fcb 100644 --- a/src/layout/Grid/config.ts +++ b/src/layout/Grid/config.ts @@ -29,6 +29,7 @@ export const Config = new CG.component({ ), ); }) + .addProperty(new CG.prop('columns', new CG.arr(CG.common('ITableColumnPropertiesWithHidden')).optional())) .addProperty(new CG.prop('rows', CG.common('GridRows'))) .extends(CG.common('LabeledComponentProps')) .extendTextResources(CG.common('TRBLabel')); diff --git a/src/layout/Grid/index.tsx b/src/layout/Grid/index.tsx index 3a4f29961c..63b14e738e 100644 --- a/src/layout/Grid/index.tsx +++ b/src/layout/Grid/index.tsx @@ -6,12 +6,13 @@ import type { ErrorObject } from 'ajv'; import { claimGridRowsChildren } from 'src/layout/Grid/claimGridRowsChildren'; import { GridDef } from 'src/layout/Grid/config.def.generated'; import { RenderGrid } from 'src/layout/Grid/GridComponent'; +import { GridLayoutValidator } from 'src/layout/Grid/GridLayoutValidator'; import { GridSummary } from 'src/layout/Grid/GridSummary'; import { GridSummaryComponent } from 'src/layout/Grid/GridSummaryComponent'; import { EmptyChildrenBoundary } from 'src/layout/Summary2/isEmpty/EmptyChildrenContext'; import type { PropsFromGenericComponent } from 'src/layout'; -import type { CompExternalExact } from 'src/layout/layout'; -import type { ChildClaimerProps, SummaryRendererProps } from 'src/layout/LayoutComponent'; +import type { CompExternalExact, NodeValidationProps } from 'src/layout/layout'; +import type { ChildClaimerProps, ExprResolver, SummaryRendererProps } from 'src/layout/LayoutComponent'; import type { Summary2Props } from 'src/layout/Summary2/SummaryComponent2/types'; export class Grid extends GridDef { @@ -41,6 +42,25 @@ export class Grid extends GridDef { claimGridRowsChildren(props, props.item.rows); } + evalExpressions(props: ExprResolver<'Grid'>) { + const { item, evalBool } = props; + + if (item.columns) { + for (const column in item.columns) { + item.columns[column].hidden = evalBool(item.columns[column].hidden, false); + } + } + + return { + ...this.evalDefaultExpressions(props), + columns: item.columns ? item.columns : undefined, + }; + } + + renderLayoutValidators(props: NodeValidationProps<'Grid'>): JSX.Element | null { + return ; + } + /** * Override layout validation to validate grid cells individually */ diff --git a/src/layout/RepeatingGroup/config.ts b/src/layout/RepeatingGroup/config.ts index 16acbaf66f..8f0da3012b 100644 --- a/src/layout/RepeatingGroup/config.ts +++ b/src/layout/RepeatingGroup/config.ts @@ -288,7 +288,7 @@ export const Config = new CG.component({ ), ), ) - .extends(CG.common('ITableColumnProperties')) + .extends(CG.common('ITableColumnPropertiesWithHidden')) .exportAs('IGroupColumnFormatting'), ) .addExample({