From c3183807729f9df621e1ee5990de79c168ce60fe Mon Sep 17 00:00:00 2001 From: cammiida Date: Tue, 3 Sep 2024 20:13:25 +0200 Subject: [PATCH 1/4] adds simpleTable MVP --- src/features/externalApi/useExternalApi.ts | 2 +- .../SimpleTable/SimpleTableComponent.tsx | 52 +++++++++++++++++++ src/layout/SimpleTable/config.ts | 43 +++++++++++++++ src/layout/SimpleTable/index.tsx | 14 +++++ 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/layout/SimpleTable/SimpleTableComponent.tsx create mode 100644 src/layout/SimpleTable/config.ts create mode 100644 src/layout/SimpleTable/index.tsx diff --git a/src/features/externalApi/useExternalApi.ts b/src/features/externalApi/useExternalApi.ts index 5844828f25..3ecd8638e1 100644 --- a/src/features/externalApi/useExternalApi.ts +++ b/src/features/externalApi/useExternalApi.ts @@ -48,7 +48,7 @@ export function useExternalApis(ids: string[]): ExternalApisResult { }); } -export function useExternalApi(id: string): unknown { +export function useExternalApi(id: string) { const instanceId = useLaxInstanceId(); return useQuery(getExternalApiQueryDef({ externalApiId: id, instanceId })); diff --git a/src/layout/SimpleTable/SimpleTableComponent.tsx b/src/layout/SimpleTable/SimpleTableComponent.tsx new file mode 100644 index 0000000000..55ccd0c239 --- /dev/null +++ b/src/layout/SimpleTable/SimpleTableComponent.tsx @@ -0,0 +1,52 @@ +import React from 'react'; + +import { Table } from '@digdir/designsystemet-react'; +import dot from 'dot-object'; + +import { Label } from 'src/components/label/Label'; +import { useExternalApi } from 'src/features/externalApi/useExternalApi'; +import { useNodeItem } from 'src/utils/layout/useNodeItem'; +import type { PropsFromGenericComponent } from 'src/layout'; + +export const SimpleTableComponent = ({ node }: PropsFromGenericComponent<'SimpleTable'>) => { + const nodeItem = useNodeItem(node); + + const { data: externalApi } = useExternalApi(nodeItem.data.id); + const data = nodeItem.data.path ? dot.pick(nodeItem.data.path, externalApi) : externalApi; + + const isValidData = + data && + Array.isArray(data) && + data.every((row) => typeof row === 'object' && nodeItem.columns.every(({ path }) => path in row)); + + if (!isValidData) { + return
Invalid component
; + } + + return ( + + ); +}; diff --git a/src/layout/SimpleTable/config.ts b/src/layout/SimpleTable/config.ts new file mode 100644 index 0000000000..b9a19d3681 --- /dev/null +++ b/src/layout/SimpleTable/config.ts @@ -0,0 +1,43 @@ +import { CG } from 'src/codegen/CG'; +import { CompCategory } from 'src/layout/common'; + +export const Config = new CG.component({ + category: CompCategory.Presentation, + capabilities: { + renderInTable: false, + renderInButtonGroup: false, + renderInAccordion: false, + renderInAccordionGroup: false, + renderInCards: false, + renderInCardsMedia: false, + renderInTabs: true, + }, + functionality: { + customExpressions: false, + }, +}) + .extends(CG.common('LabeledComponentProps')) + .addProperty(new CG.prop('title', new CG.str())) + .addProperty( + new CG.prop( + 'columns', + new CG.arr( + new CG.obj( + new CG.prop('id', new CG.str()), + new CG.prop('title', new CG.str()), + new CG.prop('type', new CG.str()), + new CG.prop('path', new CG.str()), + ), + ), + ), + ) + .addProperty( + new CG.prop( + 'data', + new CG.obj( + new CG.prop('type', new CG.const('externalApi')), + new CG.prop('id', new CG.str()), + new CG.prop('path', new CG.str()), + ), + ), + ); diff --git a/src/layout/SimpleTable/index.tsx b/src/layout/SimpleTable/index.tsx new file mode 100644 index 0000000000..5925ffc274 --- /dev/null +++ b/src/layout/SimpleTable/index.tsx @@ -0,0 +1,14 @@ +import React, { forwardRef } from 'react'; +import type { JSX } from 'react'; + +import { SimpleTableDef } from 'src/layout/SimpleTable/config.def.generated'; +import { SimpleTableComponent } from 'src/layout/SimpleTable/SimpleTableComponent'; +import type { PropsFromGenericComponent } from 'src/layout'; + +export class SimpleTable extends SimpleTableDef { + render = forwardRef>( + function LayoutComponentSimpleTableRender(props, _): JSX.Element | null { + return ; + }, + ); +} From 5c55ec911f2ab5622571bde2c7de17ccc748c965 Mon Sep 17 00:00:00 2001 From: Camilla Marie Dalan Date: Fri, 6 Sep 2024 21:24:18 +0200 Subject: [PATCH 2/4] adds support for link and text type in table --- src/codegen/dataTypes/GenerateIntersection.ts | 2 +- .../SimpleTable/SimpleTableComponent.tsx | 77 +++++++++++-------- src/layout/SimpleTable/config.ts | 17 +++- 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/src/codegen/dataTypes/GenerateIntersection.ts b/src/codegen/dataTypes/GenerateIntersection.ts index 1cec13ec07..1d0e2b31be 100644 --- a/src/codegen/dataTypes/GenerateIntersection.ts +++ b/src/codegen/dataTypes/GenerateIntersection.ts @@ -20,7 +20,7 @@ export class GenerateIntersection[]> extends Descri } toTypeScriptDefinition(symbol: string | undefined): string { - const out = this.types.map((type) => type.toTypeScript()).join(' & '); + const out = this.types.map((type) => `(${type.toTypeScript()})`).join(' & '); return symbol ? `type ${symbol} = ${out};` : out; } diff --git a/src/layout/SimpleTable/SimpleTableComponent.tsx b/src/layout/SimpleTable/SimpleTableComponent.tsx index 55ccd0c239..22ee1f119c 100644 --- a/src/layout/SimpleTable/SimpleTableComponent.tsx +++ b/src/layout/SimpleTable/SimpleTableComponent.tsx @@ -1,52 +1,67 @@ import React from 'react'; -import { Table } from '@digdir/designsystemet-react'; +import { Link, Table } from '@digdir/designsystemet-react'; import dot from 'dot-object'; -import { Label } from 'src/components/label/Label'; +import { Caption } from 'src/components/form/Caption'; import { useExternalApi } from 'src/features/externalApi/useExternalApi'; import { useNodeItem } from 'src/utils/layout/useNodeItem'; import type { PropsFromGenericComponent } from 'src/layout'; +import type { ColumnConfig } from 'src/layout/SimpleTable/config.generated'; export const SimpleTableComponent = ({ node }: PropsFromGenericComponent<'SimpleTable'>) => { const nodeItem = useNodeItem(node); const { data: externalApi } = useExternalApi(nodeItem.data.id); - const data = nodeItem.data.path ? dot.pick(nodeItem.data.path, externalApi) : externalApi; + const data: unknown = nodeItem.data.path ? dot.pick(nodeItem.data.path, externalApi) : externalApi; - const isValidData = - data && - Array.isArray(data) && - data.every((row) => typeof row === 'object' && nodeItem.columns.every(({ path }) => path in row)); + if (!data) { + return null; + } - if (!isValidData) { - return
Invalid component
; + if (!Array.isArray(data)) { + throw new Error('Data must be an array'); } return ( - + ))} + + ); }; + +function renderColumn({ component }: ColumnConfig, row: unknown) { + if (!row) { + return null; + } + + switch (component.type) { + case 'Text': + return row[component.valuePath]; + case 'Link': + return ( + + {row[component.textPath]} + + ); + } +} diff --git a/src/layout/SimpleTable/config.ts b/src/layout/SimpleTable/config.ts index b9a19d3681..3e4e85595d 100644 --- a/src/layout/SimpleTable/config.ts +++ b/src/layout/SimpleTable/config.ts @@ -25,9 +25,18 @@ export const Config = new CG.component({ new CG.obj( new CG.prop('id', new CG.str()), new CG.prop('title', new CG.str()), - new CG.prop('type', new CG.str()), - new CG.prop('path', new CG.str()), - ), + new CG.prop( + 'component', + new CG.union( + new CG.obj(new CG.prop('type', new CG.const('Text')), new CG.prop('valuePath', new CG.str())), + new CG.obj( + new CG.prop('type', new CG.const('Link')), + new CG.prop('hrefPath', new CG.str()), + new CG.prop('textPath', new CG.str()), + ), + ), + ), + ).exportAs('ColumnConfig'), ), ), ) @@ -38,6 +47,6 @@ export const Config = new CG.component({ new CG.prop('type', new CG.const('externalApi')), new CG.prop('id', new CG.str()), new CG.prop('path', new CG.str()), - ), + ).exportAs('DataConfig'), ), ); From 9c2f81c9b55f5be9fe4183875894d83b63922c90 Mon Sep 17 00:00:00 2001 From: Camilla Marie Dalan Date: Sat, 7 Sep 2024 10:46:33 +0200 Subject: [PATCH 3/4] adds typeguards and error handling to SimpleTable --- src/layout/GenericComponent.tsx | 7 +-- .../SimpleTable/SimpleTableComponent.tsx | 56 +++++++++++-------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/src/layout/GenericComponent.tsx b/src/layout/GenericComponent.tsx index 5d7c54b06b..b6c31e4217 100644 --- a/src/layout/GenericComponent.tsx +++ b/src/layout/GenericComponent.tsx @@ -63,7 +63,7 @@ function _GenericComponent({ if (generatorErrors && Object.keys(generatorErrors).length > 0) { return ( ); @@ -256,8 +256,7 @@ const gridToClasses = (labelGrid: IGridStyling | undefined, classes: { [key: str }; }; -const ErrorList = ({ node, errors }: { node: LayoutNode; errors: string[] }) => { - const id = node.id; +export const ErrorList = ({ nodeId, errors }: { nodeId: string; errors: string[] }) => { const isDev = useIsDev(); if (!isDev) { return null; @@ -268,7 +267,7 @@ const ErrorList = ({ node, errors }: { node: LayoutNode; errors: string[] }) =>

    diff --git a/src/layout/SimpleTable/SimpleTableComponent.tsx b/src/layout/SimpleTable/SimpleTableComponent.tsx index 22ee1f119c..7f3b7cda5f 100644 --- a/src/layout/SimpleTable/SimpleTableComponent.tsx +++ b/src/layout/SimpleTable/SimpleTableComponent.tsx @@ -5,6 +5,7 @@ import dot from 'dot-object'; import { Caption } from 'src/components/form/Caption'; import { useExternalApi } from 'src/features/externalApi/useExternalApi'; +import { ErrorList } from 'src/layout/GenericComponent'; import { useNodeItem } from 'src/utils/layout/useNodeItem'; import type { PropsFromGenericComponent } from 'src/layout'; import type { ColumnConfig } from 'src/layout/SimpleTable/config.generated'; @@ -13,14 +14,15 @@ export const SimpleTableComponent = ({ node }: PropsFromGenericComponent<'Simple const nodeItem = useNodeItem(node); const { data: externalApi } = useExternalApi(nodeItem.data.id); - const data: unknown = nodeItem.data.path ? dot.pick(nodeItem.data.path, externalApi) : externalApi; + const data: unknown = dot.pick(nodeItem.data.path, externalApi); - if (!data) { - return null; - } - - if (!Array.isArray(data)) { - throw new Error('Data must be an array'); + if (!isArrayOfObjects(data)) { + return ( + + ); } return ( @@ -34,10 +36,15 @@ export const SimpleTableComponent = ({ node }: PropsFromGenericComponent<'Simple + {data.length === 0 && ( + + Ingen data + + )} {data.map((row) => ( {nodeItem.columns.map((column, idx) => ( - {renderColumn(column, row)} + {renderCell(column.component, row)} ))} ))} @@ -46,22 +53,23 @@ export const SimpleTableComponent = ({ node }: PropsFromGenericComponent<'Simple ); }; -function renderColumn({ component }: ColumnConfig, row: unknown) { - if (!row) { - return null; +function renderCell(component: ColumnConfig['component'], row: Record) { + if (component.type === 'Link') { + return ( + + {dot.pick(component.textPath, row)} + + ); } - switch (component.type) { - case 'Text': - return row[component.valuePath]; - case 'Link': - return ( - - {row[component.textPath]} - - ); - } + // FIXME: what if the path does not exist and the resulting value is undefined? + // Should we display an error, or is this a valid use case in production? + return dot.pick(component.valuePath, row); +} + +function isArrayOfObjects(data: unknown): data is Record[] { + return Array.isArray(data) && data.every((row: unknown) => !!row && typeof row === 'object' && !Array.isArray(row)); } From 6f3a68a43a91621406b1b73753785c3be09c060c Mon Sep 17 00:00:00 2001 From: Camilla Marie Dalan Date: Mon, 9 Sep 2024 10:46:10 +0200 Subject: [PATCH 4/4] adds discriminated union for component types in simple table --- src/layout/SimpleTable/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layout/SimpleTable/config.ts b/src/layout/SimpleTable/config.ts index 3e4e85595d..51f848f3b8 100644 --- a/src/layout/SimpleTable/config.ts +++ b/src/layout/SimpleTable/config.ts @@ -34,7 +34,7 @@ export const Config = new CG.component({ new CG.prop('hrefPath', new CG.str()), new CG.prop('textPath', new CG.str()), ), - ), + ).setUnionType('discriminated'), ), ).exportAs('ColumnConfig'), ),