From 089bf5166fd6b1763a2a5aa3a32b9fa87a35eecf Mon Sep 17 00:00:00 2001 From: lassopicasso Date: Wed, 4 Mar 2026 09:58:35 +0100 Subject: [PATCH 1/8] support colSpan without expression --- src/codegen/Common.ts | 7 +++++++ src/layout/Grid/GridComponent.tsx | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/codegen/Common.ts b/src/codegen/Common.ts index 2f5d53021d..3dfaca42e7 100644 --- a/src/codegen/Common.ts +++ b/src/codegen/Common.ts @@ -475,6 +475,13 @@ const common = { 'column, and if it evaluates to true, the column will be hidden.', ), ), + new CG.prop( + 'colSpan', + new CG.expr(ExprVal.Number) + .optional() + .setTitle('Column span') + .setDescription('Number of columns this cell should span. Defaults to 1 if not set.'), + ), ) .setTitle('Column options') .setDescription('Options for the row/column') diff --git a/src/layout/Grid/GridComponent.tsx b/src/layout/Grid/GridComponent.tsx index fcf18e3976..bc46999811 100644 --- a/src/layout/Grid/GridComponent.tsx +++ b/src/layout/Grid/GridComponent.tsx @@ -241,6 +241,7 @@ function CellWithComponent({ }: CellWithComponentProps) { const isHidden = useIsHidden(baseComponentId); const CellComponent = isHeader ? Table.HeaderCell : Table.Cell; + const colSpanValue = columnStyleOptions?.colSpan as number | undefined; if (!isHidden) { const columnStyles = columnStyleOptions && getColumnStyles(columnStyleOptions); @@ -248,6 +249,7 @@ function CellWithComponent({ Date: Wed, 4 Mar 2026 14:54:27 +0100 Subject: [PATCH 2/8] support colSpan without expression --- src/layout/Grid/GridComponent.tsx | 8 ++++++++ src/layout/Grid/index.tsx | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/layout/Grid/GridComponent.tsx b/src/layout/Grid/GridComponent.tsx index bc46999811..5511cf0e99 100644 --- a/src/layout/Grid/GridComponent.tsx +++ b/src/layout/Grid/GridComponent.tsx @@ -30,6 +30,8 @@ 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 { useEvalExpression } from 'src/utils/layout/generator/useEvalExpression'; +import { ExprVal } from 'src/features/expressions/types'; export function RenderGrid(props: PropsFromGenericComponent<'Grid'>) { const { baseComponentId } = props; @@ -268,6 +270,12 @@ function CellWithComponent({ } function CellWithText({ children, className, columnStyleOptions, help, isHeader = false }: CellWithTextProps) { + // const colSpanValue = useEvalExpression(columnStyleOptions?.colSpan, { + // returnType: ExprVal.Number, + // defaultValue: 1, + // errorIntroText: `Invalid expression for colSpan in Grid cell with text "${children}"`, + // }); + const columnStyles = columnStyleOptions && getColumnStyles(columnStyleOptions); const { elementAsString } = useLanguage(); const CellComponent = isHeader ? Table.HeaderCell : Table.Cell; diff --git a/src/layout/Grid/index.tsx b/src/layout/Grid/index.tsx index 3a4f29961c..bd46b1b66c 100644 --- a/src/layout/Grid/index.tsx +++ b/src/layout/Grid/index.tsx @@ -11,8 +11,10 @@ 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 { ChildClaimerProps, ExprResolver, SummaryRendererProps } from 'src/layout/LayoutComponent'; import type { Summary2Props } from 'src/layout/Summary2/SummaryComponent2/types'; +import { ExprResolved } from 'src/features/expressions/types'; +import { ITableColumnProperties } from '../common.generated'; export class Grid extends GridDef { render = forwardRef>( From dc40cec70326d785a2f81db0c82b6bb8d55b58f7 Mon Sep 17 00:00:00 2001 From: lassopicasso Date: Wed, 4 Mar 2026 17:15:58 +0100 Subject: [PATCH 3/8] support colSpan without expression --- src/codegen/Common.ts | 29 ++++++++++------ src/layout/Grid/GridComponent.tsx | 57 +++++++++++++++++++++++++------ 2 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/codegen/Common.ts b/src/codegen/Common.ts index 3dfaca42e7..c3e6a83d97 100644 --- a/src/codegen/Common.ts +++ b/src/codegen/Common.ts @@ -425,6 +425,19 @@ const common = { ), ).extends(CG.common('ISelectionComponent')), + IGridColumnProperties: () => + new CG.obj( + new CG.prop( + 'colSpan', + new CG.expr(ExprVal.Number) + .optional() + .setTitle('Column span') + .setDescription('Number of columns this cell should span. Defaults to 1 if not set.'), + ), + ) + .setTitle('Grid column properties') + .setDescription('Additional properties for columns in the Grid component'), + // Table configuration: ITableColumnsAlignText: () => new CG.enum('left', 'center', 'right') @@ -475,13 +488,6 @@ const common = { 'column, and if it evaluates to true, the column will be hidden.', ), ), - new CG.prop( - 'colSpan', - new CG.expr(ExprVal.Number) - .optional() - .setTitle('Column span') - .setDescription('Number of columns this cell should span. Defaults to 1 if not set.'), - ), ) .setTitle('Column options') .setDescription('Options for the row/column') @@ -595,7 +601,8 @@ const common = { new CG.obj( new CG.prop('component', new CG.str().optional().setTitle('Component ID').setDescription('ID of the component')), new CG.prop('columnOptions', CG.common('ITableColumnProperties').optional()), - ).extends(CG.common('ITableColumnProperties')), + new CG.prop('gridColumnOptions', CG.common('IGridColumnProperties').optional()), + ), GridCellLabelFrom: () => new CG.obj( new CG.prop( @@ -605,7 +612,8 @@ const common = { .setDescription('Set this to a component id to display the label from that component'), ), new CG.prop('columnOptions', CG.common('ITableColumnProperties').optional()), - ).extends(CG.common('ITableColumnProperties')), + new CG.prop('gridColumnOptions', CG.common('IGridColumnProperties').optional()), + ), GridCellText: () => new CG.obj( new CG.prop( @@ -614,7 +622,8 @@ const common = { ), new CG.prop('help', new CG.str().optional().setTitle('Help').setDescription('Help text to display')), new CG.prop('columnOptions', CG.common('ITableColumnProperties').optional()), - ).extends(CG.common('ITableColumnProperties')), + new CG.prop('gridColumnOptions', CG.common('IGridColumnProperties').optional()), + ), GridCell: () => new CG.union(CG.common('GridComponentRef'), CG.null, CG.common('GridCellText'), CG.common('GridCellLabelFrom')), GridRow: () => diff --git a/src/layout/Grid/GridComponent.tsx b/src/layout/Grid/GridComponent.tsx index 5511cf0e99..5bd8e9c5b6 100644 --- a/src/layout/Grid/GridComponent.tsx +++ b/src/layout/Grid/GridComponent.tsx @@ -29,7 +29,13 @@ 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, + IGridColumnProperties, + ITableColumnFormatting, + ITableColumnProperties, +} from 'src/layout/common.generated'; import { useEvalExpression } from 'src/utils/layout/generator/useEvalExpression'; import { ExprVal } from 'src/features/expressions/types'; @@ -158,12 +164,15 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd mutableColumnSettings[cellIdx] = cell.columnOptions; } + const gridColumnOptions = cell && 'gridColumnOptions' in cell ? cell.gridColumnOptions : undefined; + if (isGridCellText(cell) || isGridCellLabelFrom(cell)) { let textCellSettings: ITableColumnProperties = mutableColumnSettings[cellIdx] ? structuredClone(mutableColumnSettings[cellIdx]) : {}; textCellSettings = { ...textCellSettings, ...cell }; + console.log(cell, textCellSettings); if (isGridCellText(cell)) { return ( @@ -185,6 +195,7 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd isHeader={row.header} columnStyleOptions={textCellSettings} labelFrom={cell.labelFrom} + gridColumnOptions={gridColumnOptions} /> ); } @@ -208,6 +219,7 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd isHeader={row.header} className={className} columnStyleOptions={mutableColumnSettings[cellIdx]} + gridColumnOptions={gridColumnOptions} /> ); })} @@ -218,6 +230,7 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd interface CellProps { className?: string; columnStyleOptions?: ITableColumnProperties; + gridColumnOptions?: IGridColumnProperties; isHeader?: boolean; rowReadOnly?: boolean; } @@ -238,12 +251,18 @@ function CellWithComponent({ baseComponentId, className, columnStyleOptions, + gridColumnOptions, isHeader = false, rowReadOnly, }: CellWithComponentProps) { const isHidden = useIsHidden(baseComponentId); const CellComponent = isHeader ? Table.HeaderCell : Table.Cell; - const colSpanValue = columnStyleOptions?.colSpan as number | undefined; + const colSpanValue = useEvalExpression(gridColumnOptions?.colSpan, { + returnType: ExprVal.Number, + defaultValue: 1, + errorIntroText: `Invalid expression for colSpan in Grid cell with component "${baseComponentId}"`, + }); + // const colSpanValue = columnStyleOptions?.colSpan as number | undefined; if (!isHidden) { const columnStyles = columnStyleOptions && getColumnStyles(columnStyleOptions); @@ -269,17 +288,23 @@ function CellWithComponent({ return ; } -function CellWithText({ children, className, columnStyleOptions, help, isHeader = false }: CellWithTextProps) { - // const colSpanValue = useEvalExpression(columnStyleOptions?.colSpan, { - // returnType: ExprVal.Number, - // defaultValue: 1, - // errorIntroText: `Invalid expression for colSpan in Grid cell with text "${children}"`, - // }); +function CellWithText({ + children, + className, + columnStyleOptions, + gridColumnOptions, + help, + isHeader = false, +}: CellWithTextProps) { + const colSpanValue = useEvalExpression(gridColumnOptions?.colSpan, { + returnType: ExprVal.Number, + defaultValue: 1, + errorIntroText: `Invalid expression for colSpan in Grid cell with text "${children}"`, + }); const columnStyles = columnStyleOptions && getColumnStyles(columnStyleOptions); const { elementAsString } = useLanguage(); const CellComponent = isHeader ? Table.HeaderCell : Table.Cell; - const colSpanValue = columnStyleOptions?.colSpan as number | undefined; return ( Date: Mon, 9 Mar 2026 08:38:21 +0100 Subject: [PATCH 4/8] support colSpan with expression --- src/layout/Grid/GridComponent.tsx | 42 ++++++++----------------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/src/layout/Grid/GridComponent.tsx b/src/layout/Grid/GridComponent.tsx index 5bd8e9c5b6..080c4b905b 100644 --- a/src/layout/Grid/GridComponent.tsx +++ b/src/layout/Grid/GridComponent.tsx @@ -10,6 +10,7 @@ import { Fieldset } from 'src/app-components/Label/Fieldset'; import { Caption } from 'src/components/form/caption/Caption'; import { HelpTextContainer } from 'src/components/form/HelpTextContainer'; import { LabelContent } from 'src/components/label/LabelContent'; +import { ExprVal } from 'src/features/expressions/types'; import { useLayoutLookups } from 'src/features/form/layout/LayoutsContext'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; @@ -25,6 +26,7 @@ import { } from 'src/layout/Grid/tools'; import { getColumnStyles } from 'src/utils/formComponentUtils'; import { useIndexedId } from 'src/utils/layout/DataModelLocation'; +import { useEvalExpression } from 'src/utils/layout/generator/useEvalExpression'; import { useIsHidden } from 'src/utils/layout/hidden'; import { useLabel } from 'src/utils/layout/useLabel'; import { useItemFor, useItemWhenType } from 'src/utils/layout/useNodeItem'; @@ -36,8 +38,6 @@ import type { ITableColumnFormatting, ITableColumnProperties, } from 'src/layout/common.generated'; -import { useEvalExpression } from 'src/utils/layout/generator/useEvalExpression'; -import { ExprVal } from 'src/features/expressions/types'; export function RenderGrid(props: PropsFromGenericComponent<'Grid'>) { const { baseComponentId } = props; @@ -164,15 +164,12 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd mutableColumnSettings[cellIdx] = cell.columnOptions; } - const gridColumnOptions = cell && 'gridColumnOptions' in cell ? cell.gridColumnOptions : undefined; - if (isGridCellText(cell) || isGridCellLabelFrom(cell)) { - let textCellSettings: ITableColumnProperties = mutableColumnSettings[cellIdx] + let textCellSettings: GridColumnOptions = mutableColumnSettings[cellIdx] ? structuredClone(mutableColumnSettings[cellIdx]) : {}; textCellSettings = { ...textCellSettings, ...cell }; - console.log(cell, textCellSettings); if (isGridCellText(cell)) { return ( @@ -195,7 +191,6 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd isHeader={row.header} columnStyleOptions={textCellSettings} labelFrom={cell.labelFrom} - gridColumnOptions={gridColumnOptions} /> ); } @@ -219,7 +214,6 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd isHeader={row.header} className={className} columnStyleOptions={mutableColumnSettings[cellIdx]} - gridColumnOptions={gridColumnOptions} /> ); })} @@ -229,12 +223,13 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd interface CellProps { className?: string; - columnStyleOptions?: ITableColumnProperties; - gridColumnOptions?: IGridColumnProperties; + columnStyleOptions?: GridColumnOptions; isHeader?: boolean; rowReadOnly?: boolean; } +type GridColumnOptions = ITableColumnProperties & IGridColumnProperties; + interface CellWithComponentProps extends CellProps { baseComponentId: string; } @@ -251,18 +246,16 @@ function CellWithComponent({ baseComponentId, className, columnStyleOptions, - gridColumnOptions, isHeader = false, rowReadOnly, }: CellWithComponentProps) { const isHidden = useIsHidden(baseComponentId); const CellComponent = isHeader ? Table.HeaderCell : Table.Cell; - const colSpanValue = useEvalExpression(gridColumnOptions?.colSpan, { + const colSpanValue = useEvalExpression(columnStyleOptions?.colSpan, { returnType: ExprVal.Number, defaultValue: 1, errorIntroText: `Invalid expression for colSpan in Grid cell with component "${baseComponentId}"`, }); - // const colSpanValue = columnStyleOptions?.colSpan as number | undefined; if (!isHidden) { const columnStyles = columnStyleOptions && getColumnStyles(columnStyleOptions); @@ -288,15 +281,8 @@ function CellWithComponent({ return ; } -function CellWithText({ - children, - className, - columnStyleOptions, - gridColumnOptions, - help, - isHeader = false, -}: CellWithTextProps) { - const colSpanValue = useEvalExpression(gridColumnOptions?.colSpan, { +function CellWithText({ children, className, columnStyleOptions, help, isHeader = false }: CellWithTextProps) { + const colSpanValue = useEvalExpression(columnStyleOptions?.colSpan, { returnType: ExprVal.Number, defaultValue: 1, errorIntroText: `Invalid expression for colSpan in Grid cell with text "${children}"`, @@ -330,18 +316,12 @@ function CellWithText({ ); } -function CellWithLabel({ - className, - columnStyleOptions, - gridColumnOptions, - labelFrom, - isHeader = false, -}: CellWithLabelProps) { +function CellWithLabel({ className, columnStyleOptions, labelFrom, isHeader = false }: CellWithLabelProps) { const columnStyles = columnStyleOptions && getColumnStyles(columnStyleOptions); const item = useItemFor(labelFrom); const trb = item.textResourceBindings; const required = 'required' in item && item.required; - const colSpanValue = useEvalExpression(gridColumnOptions?.colSpan, { + const colSpanValue = useEvalExpression(columnStyleOptions?.colSpan, { returnType: ExprVal.Number, defaultValue: 1, errorIntroText: `Invalid expression for colSpan in Grid cell with label from "${labelFrom}"`, From 859546ad70cf2bb7d4f1ccff50ec588bc8289a64 Mon Sep 17 00:00:00 2001 From: Jamal Alabdullah Date: Tue, 17 Mar 2026 09:31:07 +0100 Subject: [PATCH 5/8] update colSpan and add hidden for column --- src/codegen/Common.ts | 9 ++++ src/layout/Grid/GridComponent.tsx | 69 ++++++++++++++++++++++++++++--- src/layout/Grid/tools.ts | 11 +++++ 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/src/codegen/Common.ts b/src/codegen/Common.ts index c3e6a83d97..531f84bab4 100644 --- a/src/codegen/Common.ts +++ b/src/codegen/Common.ts @@ -434,6 +434,15 @@ const common = { .setTitle('Column span') .setDescription('Number of columns this cell should span. Defaults to 1 if not set.'), ), + new CG.prop( + 'hidden', + new CG.expr(ExprVal.Boolean) + .optional() + .setTitle('Hidden column') + .setDescription( + 'Expression or boolean indicating whether this column should be hidden. Defaults to false if not set.', + ), + ), ) .setTitle('Grid column properties') .setDescription('Additional properties for columns in the Grid component'), diff --git a/src/layout/Grid/GridComponent.tsx b/src/layout/Grid/GridComponent.tsx index 080c4b905b..f7f715d8c6 100644 --- a/src/layout/Grid/GridComponent.tsx +++ b/src/layout/Grid/GridComponent.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import type { PropsWithChildren } from 'react'; import { Table } from '@digdir/designsystemet-react'; @@ -10,7 +10,9 @@ import { Fieldset } from 'src/app-components/Label/Fieldset'; import { Caption } from 'src/components/form/caption/Caption'; import { HelpTextContainer } from 'src/components/form/HelpTextContainer'; import { LabelContent } from 'src/components/label/LabelContent'; +import { evalExpr } from 'src/features/expressions'; import { ExprVal } from 'src/features/expressions/types'; +import { ExprValidation } from 'src/features/expressions/validation'; import { useLayoutLookups } from 'src/features/form/layout/LayoutsContext'; import { Lang } from 'src/features/language/Lang'; import { useLanguage } from 'src/features/language/useLanguage'; @@ -18,6 +20,7 @@ import { useIsMobile } from 'src/hooks/useDeviceWidths'; import { GenericComponent } from 'src/layout/GenericComponent'; import css from 'src/layout/Grid/Grid.module.css'; import { + getGridCellHiddenExpr, isGridCellLabelFrom, isGridCellNode, isGridCellText, @@ -28,6 +31,7 @@ import { getColumnStyles } from 'src/utils/formComponentUtils'; import { useIndexedId } from 'src/utils/layout/DataModelLocation'; import { useEvalExpression } from 'src/utils/layout/generator/useEvalExpression'; import { useIsHidden } from 'src/utils/layout/hidden'; +import { useExpressionDataSources } from 'src/utils/layout/useExpressionDataSources'; import { useLabel } from 'src/utils/layout/useLabel'; import { useItemFor, useItemWhenType } from 'src/utils/layout/useNodeItem'; import type { PropsFromGenericComponent } from 'src/layout'; @@ -52,6 +56,27 @@ export function RenderGrid(props: PropsFromGenericComponent<'Grid'>) { const accessibleTitle = elementAsString(title); const indexedId = useIndexedId(baseComponentId); + const columnHiddenExprs = useMemo(() => rows?.find((r) => r.header)?.cells?.map(getGridCellHiddenExpr) ?? [], [rows]); + const expressionDataSources = useExpressionDataSources(columnHiddenExprs); + const hiddenColumnIndices = useMemo( + () => + columnHiddenExprs.reduce((indices, hiddenExpr, cellIdx) => { + if (!ExprValidation.isValidOrScalar(hiddenExpr, ExprVal.Boolean)) { + return indices; + } + const hidden = evalExpr(hiddenExpr, expressionDataSources, { + returnType: ExprVal.Boolean, + defaultValue: false, + errorIntroText: `Invalid expression for hidden in Grid column ${cellIdx}`, + }); + if (hidden) { + indices.push(cellIdx); + } + return indices; + }, []), + [columnHiddenExprs, expressionDataSources], + ); + if (isMobile) { return ; } @@ -78,6 +103,7 @@ export function RenderGrid(props: PropsFromGenericComponent<'Grid'>) { rows={rows} isNested={isNested} mutableColumnSettings={columnSettings} + hiddenColumnIndices={hiddenColumnIndices} /> @@ -165,10 +191,23 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd } if (isGridCellText(cell) || isGridCellLabelFrom(cell)) { - let textCellSettings: GridColumnOptions = mutableColumnSettings[cellIdx] - ? structuredClone(mutableColumnSettings[cellIdx]) - : {}; - textCellSettings = { ...textCellSettings, ...cell }; + let textCellSettings: GridColumnOptions | undefined = + mutableColumnSettings[cellIdx] && structuredClone(mutableColumnSettings[cellIdx]); + + if (cell && 'gridColumnOptions' in cell && cell.gridColumnOptions) { + textCellSettings = { + ...(textCellSettings ?? {}), + ...cell.gridColumnOptions, + }; + } + + const cellWithColSpan = cell as { colSpan?: number } | null; + if (cellWithColSpan && typeof cellWithColSpan.colSpan !== 'undefined') { + textCellSettings = { + ...(textCellSettings ?? {}), + colSpan: cellWithColSpan.colSpan, + }; + } if (isGridCellText(cell)) { return ( @@ -206,6 +245,24 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd ); } + let componentCellSettings: GridColumnOptions | undefined = + mutableColumnSettings[cellIdx] && structuredClone(mutableColumnSettings[cellIdx]); + + if (cell && 'gridColumnOptions' in cell && cell.gridColumnOptions) { + componentCellSettings = { + ...(componentCellSettings ?? {}), + ...cell.gridColumnOptions, + }; + } + + const cellWithColSpan = cell as { colSpan?: number } | null; + if (cellWithColSpan && typeof cellWithColSpan.colSpan !== 'undefined') { + componentCellSettings = { + ...(componentCellSettings ?? {}), + colSpan: cellWithColSpan.colSpan, + }; + } + return ( ); })} diff --git a/src/layout/Grid/tools.ts b/src/layout/Grid/tools.ts index bb203aecea..fdf5c1b4e0 100644 --- a/src/layout/Grid/tools.ts +++ b/src/layout/Grid/tools.ts @@ -105,3 +105,14 @@ export function isGridCellEmpty(cell: GridCell): boolean { export function isGridCellNode(cell: GridCell): cell is GridComponentRef { return !!(cell && 'component' in cell && cell.component); } + +export function getGridCellHiddenExpr(cell: GridCell) { + if (!cell || typeof cell !== 'object') { + return undefined; + } + const options = + 'columnOptions' in cell ? (cell as { columnOptions?: { hidden?: unknown } }).columnOptions : undefined; + const gridOpts = + 'gridColumnOptions' in cell ? (cell as { gridColumnOptions?: { hidden?: unknown } }).gridColumnOptions : undefined; + return gridOpts?.hidden ?? options?.hidden; +} From e036442f193eac31b5eefce3fdfaa0f1815474f4 Mon Sep 17 00:00:00 2001 From: Jamal Alabdullah Date: Tue, 17 Mar 2026 09:43:45 +0100 Subject: [PATCH 6/8] removed unused imports --- src/layout/Grid/index.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/layout/Grid/index.tsx b/src/layout/Grid/index.tsx index bd46b1b66c..3a4f29961c 100644 --- a/src/layout/Grid/index.tsx +++ b/src/layout/Grid/index.tsx @@ -11,10 +11,8 @@ 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, ExprResolver, SummaryRendererProps } from 'src/layout/LayoutComponent'; +import type { ChildClaimerProps, SummaryRendererProps } from 'src/layout/LayoutComponent'; import type { Summary2Props } from 'src/layout/Summary2/SummaryComponent2/types'; -import { ExprResolved } from 'src/features/expressions/types'; -import { ITableColumnProperties } from '../common.generated'; export class Grid extends GridDef { render = forwardRef>( From 912170e2b5b8db6ab60a7df33ad6c6c1cba4e3c3 Mon Sep 17 00:00:00 2001 From: Jamal Alabdullah Date: Tue, 17 Mar 2026 11:39:06 +0100 Subject: [PATCH 7/8] added test --- src/layout/Grid/GridComponent.test.tsx | 152 +++++++++++++++++++++++++ src/layout/Grid/GridComponent.tsx | 36 +++--- src/layout/Grid/tools.test.ts | 23 ++++ 3 files changed, 191 insertions(+), 20 deletions(-) create mode 100644 src/layout/Grid/GridComponent.test.tsx create mode 100644 src/layout/Grid/tools.test.ts diff --git a/src/layout/Grid/GridComponent.test.tsx b/src/layout/Grid/GridComponent.test.tsx new file mode 100644 index 0000000000..f75748c4a0 --- /dev/null +++ b/src/layout/Grid/GridComponent.test.tsx @@ -0,0 +1,152 @@ +import React from 'react'; + +import { screen } from '@testing-library/react'; + +import { RenderGrid } from 'src/layout/Grid/GridComponent'; +import { renderGenericComponentTest } from 'src/test/renderWithProviders'; +import type { CompExternalExact } from 'src/layout/layout'; + +describe('GridComponent', () => { + const render = async (hiddenValue: unknown) => + await renderGenericComponentTest({ + type: 'Grid', + renderer: (props) => , + component: { + rows: [ + { + header: true, + readOnly: false, + cells: [ + { text: 'accordion.title' }, + { + text: 'FormLayout', + gridColumnOptions: { hidden: hiddenValue }, + }, + ], + }, + { + header: false, + readOnly: false, + cells: [{ text: 'accordion.title' }, { text: 'FormLayout' }], + }, + ], + } as CompExternalExact<'Grid'>, + }); + + it('hides a column when header cell hidden evaluates to true', async () => { + await render(true); + const headers = screen.getAllByRole('columnheader'); + expect(headers).toHaveLength(1); + + const titleOccurrences = screen.getAllByText('This is a title'); + expect(titleOccurrences).toHaveLength(2); + expect(screen.queryByText('This is a page title')).not.toBeInTheDocument(); + + const bodyCells = screen.getAllByRole('cell'); + expect(bodyCells).toHaveLength(1); + expect(screen.getAllByText('This is a title')[0]).toBeInTheDocument(); + }); + + it('does not hide a column when hidden expression is invalid for boolean', async () => { + await render(false); + + const headers = screen.getAllByRole('columnheader'); + expect(headers).toHaveLength(2); + + const titleOccurrences = screen.getAllByText('This is a title'); + expect(titleOccurrences.length).toBeGreaterThanOrEqual(1); + const pageTitleOccurrences = screen.getAllByText('This is a page title'); + expect(pageTitleOccurrences.length).toBeGreaterThanOrEqual(1); + }); + + it('applies colSpan from text cell settings', async () => { + await renderGenericComponentTest({ + type: 'Grid', + renderer: (props) => , + component: { + rows: [ + { + header: true, + readOnly: false, + cells: [ + { + text: 'accordion.title', + colSpan: 2, + }, + { text: 'FormLayout' }, + ], + }, + ], + } as CompExternalExact<'Grid'>, + }); + + const headers = screen.getAllByRole('columnheader'); + expect(headers.length).toBeGreaterThanOrEqual(1); + const firstHeaderCell = headers[0]; + expect(firstHeaderCell).toHaveAttribute('colspan', '2'); + }); + + it('applies colSpan for component cells', async () => { + await renderGenericComponentTest({ + type: 'Grid', + renderer: (props) => , + component: { + rows: [ + { + header: false, + readOnly: false, + cells: [ + { + component: 'grid-text', + gridColumnOptions: { + colSpan: 3, + }, + }, + ], + }, + ], + } as CompExternalExact<'Grid'>, + queries: { + fetchLayouts: async () => ({ + FormLayout: { + data: { + layout: [ + { + id: 'my-test-component-id', + type: 'Grid', + rows: [ + { + header: false, + readOnly: false, + cells: [ + { + component: 'grid-text', + gridColumnOptions: { + colSpan: 3, + }, + }, + ], + }, + ], + }, + { + id: 'grid-text', + type: 'Text', + value: '', + textResourceBindings: { + title: 'accordion.title', + }, + }, + ], + }, + }, + }), + }, + }); + + const cells = screen.getAllByRole('cell'); + expect(cells.length).toBeGreaterThanOrEqual(1); + const firstCell = cells[0]; + expect(firstCell).toHaveAttribute('colspan', '3'); + }); +}); diff --git a/src/layout/Grid/GridComponent.tsx b/src/layout/Grid/GridComponent.tsx index f7f715d8c6..b2ec89c329 100644 --- a/src/layout/Grid/GridComponent.tsx +++ b/src/layout/Grid/GridComponent.tsx @@ -195,18 +195,16 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd mutableColumnSettings[cellIdx] && structuredClone(mutableColumnSettings[cellIdx]); if (cell && 'gridColumnOptions' in cell && cell.gridColumnOptions) { - textCellSettings = { - ...(textCellSettings ?? {}), - ...cell.gridColumnOptions, - }; + textCellSettings = textCellSettings + ? { ...textCellSettings, ...cell.gridColumnOptions } + : { ...cell.gridColumnOptions }; } const cellWithColSpan = cell as { colSpan?: number } | null; - if (cellWithColSpan && typeof cellWithColSpan.colSpan !== 'undefined') { - textCellSettings = { - ...(textCellSettings ?? {}), - colSpan: cellWithColSpan.colSpan, - }; + if (cellWithColSpan && cellWithColSpan.colSpan !== undefined) { + textCellSettings = textCellSettings + ? { ...textCellSettings, colSpan: cellWithColSpan.colSpan } + : { colSpan: cellWithColSpan.colSpan }; } if (isGridCellText(cell)) { @@ -249,18 +247,16 @@ function GridRowRenderer({ row, isNested, mutableColumnSettings, hiddenColumnInd mutableColumnSettings[cellIdx] && structuredClone(mutableColumnSettings[cellIdx]); if (cell && 'gridColumnOptions' in cell && cell.gridColumnOptions) { - componentCellSettings = { - ...(componentCellSettings ?? {}), - ...cell.gridColumnOptions, - }; + componentCellSettings = componentCellSettings + ? { ...componentCellSettings, ...cell.gridColumnOptions } + : { ...cell.gridColumnOptions }; } - const cellWithColSpan = cell as { colSpan?: number } | null; - if (cellWithColSpan && typeof cellWithColSpan.colSpan !== 'undefined') { - componentCellSettings = { - ...(componentCellSettings ?? {}), - colSpan: cellWithColSpan.colSpan, - }; + const cellColSpan = (cell as { colSpan?: number } | null)?.colSpan; + if (cellColSpan !== undefined) { + componentCellSettings = componentCellSettings + ? { ...componentCellSettings, colSpan: cellColSpan } + : { colSpan: cellColSpan }; } return ( @@ -342,7 +338,7 @@ function CellWithText({ children, className, columnStyleOptions, help, isHeader const colSpanValue = useEvalExpression(columnStyleOptions?.colSpan, { returnType: ExprVal.Number, defaultValue: 1, - errorIntroText: `Invalid expression for colSpan in Grid cell with text "${children}"`, + errorIntroText: 'Invalid expression for colSpan in Grid text cell', }); const columnStyles = columnStyleOptions && getColumnStyles(columnStyleOptions); diff --git a/src/layout/Grid/tools.test.ts b/src/layout/Grid/tools.test.ts new file mode 100644 index 0000000000..3c27e58a36 --- /dev/null +++ b/src/layout/Grid/tools.test.ts @@ -0,0 +1,23 @@ +import { getGridCellHiddenExpr } from 'src/layout/Grid/tools'; +import type { GridCell } from 'src/layout/common.generated'; + +describe('getGridCellHiddenExpr', () => { + it('returns undefined for non-object or null cells', () => { + expect(getGridCellHiddenExpr(null as unknown as GridCell)).toBeUndefined(); + expect(getGridCellHiddenExpr(undefined as unknown as GridCell)).toBeUndefined(); + expect(getGridCellHiddenExpr('text' as unknown as GridCell)).toBeUndefined(); + }); + + it('reads hidden from columnOptions when present', () => { + const cell = { columnOptions: { hidden: true } } as GridCell; + expect(getGridCellHiddenExpr(cell)).toBe(true); + }); + + it('reads hidden from gridColumnOptions and prefers it over columnOptions', () => { + const cell = { + columnOptions: { hidden: false }, + gridColumnOptions: { hidden: true }, + } as GridCell; + expect(getGridCellHiddenExpr(cell)).toBe(true); + }); +}); From 8b3b5f36ef6de89ea0a8a9c2eab432a73aad96e3 Mon Sep 17 00:00:00 2001 From: Jamal Alabdullah Date: Thu, 19 Mar 2026 15:32:10 +0100 Subject: [PATCH 8/8] test lowercase --- .github/scripts/release.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh index 13d1b53b52..238d57b448 100755 --- a/.github/scripts/release.sh +++ b/.github/scripts/release.sh @@ -52,6 +52,7 @@ CURRENT_VERSION_PARTS=(${CURRENT_VERSION//./ }) APP_FULL=${CURRENT_VERSION:1} APP_MAJOR=${CURRENT_VERSION_PARTS[0]:1} APP_MAJOR_MINOR=${CURRENT_VERSION_PARTS[0]:1}.${CURRENT_VERSION_PARTS[1]} +APP_FULL=$(echo "$APP_FULL" | tr '[:upper:]' '[:lower:]') echo "Git tag: '$CURRENT_VERSION'" echo "Full version: '$APP_FULL'"