Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simple table with data from external API #2416

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/codegen/dataTypes/GenerateIntersection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class GenerateIntersection<U extends CodeGenerator<any>[]> 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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/features/externalApi/useExternalApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
Expand Down
7 changes: 3 additions & 4 deletions src/layout/GenericComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function _GenericComponent<Type extends CompTypes = CompTypes>({
if (generatorErrors && Object.keys(generatorErrors).length > 0) {
return (
<ErrorList
node={node}
nodeId={node.id}
errors={Object.keys(generatorErrors)}
/>
);
Expand Down Expand Up @@ -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;
Expand All @@ -268,7 +267,7 @@ const ErrorList = ({ node, errors }: { node: LayoutNode; errors: string[] }) =>
<h3>
<Lang
id={'config_error.component_has_errors'}
params={[id]}
params={[nodeId]}
/>
</h3>
<ul>
Expand Down
75 changes: 75 additions & 0 deletions src/layout/SimpleTable/SimpleTableComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React from 'react';

import { Link, Table } from '@digdir/designsystemet-react';
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';

export const SimpleTableComponent = ({ node }: PropsFromGenericComponent<'SimpleTable'>) => {
const nodeItem = useNodeItem(node);

const { data: externalApi } = useExternalApi(nodeItem.data.id);
const data: unknown = dot.pick(nodeItem.data.path, externalApi);

if (!isArrayOfObjects(data)) {
return (
<ErrorList
nodeId={node.id}
errors={['Tabelldata må være en liste av objekter']}
/>
);
Comment on lines +20 to +25
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of rendering out an ErrorList, just throw an error instead. It will be caught in the error handler, logged to devtools, and then displayed in an error list. I would prefer not to export the ErrorList, as it just makes it possible to circumvent the normal way these errors are caught and logged.

}

return (
<Table width='100%'>
<Caption title={nodeItem.title} />
<Table.Head>
<Table.Row>
{nodeItem.columns.map((column) => (
<Table.HeaderCell key={column.id}>{column.title}</Table.HeaderCell>
))}
</Table.Row>
</Table.Head>
<Table.Body>
{data.length === 0 && (
<Table.Row>
<Table.Cell colSpan={nodeItem.columns.length}>Ingen data</Table.Cell>
</Table.Row>
)}
{data.map((row) => (
<Table.Row key={String(row)}>
{nodeItem.columns.map((column, idx) => (
<Table.Cell key={`${column.id}[${idx}]`}>{renderCell(column.component, row)}</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
);
};

function renderCell(component: ColumnConfig['component'], row: Record<string, unknown>) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably just be a react component instead. 🙌

if (component.type === 'Link') {
return (
<Link
href={dot.pick(component.hrefPath, row)}
target='_blank'
>
{dot.pick(component.textPath, row)}
</Link>
);
}

// 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<string, unknown>[] {
return Array.isArray(data) && data.every((row: unknown) => !!row && typeof row === 'object' && !Array.isArray(row));
}
52 changes: 52 additions & 0 deletions src/layout/SimpleTable/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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(
'component',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would probably call this content instead of component. The latter has a specific meaning for app developers, and we use it in Grid to point to other components in the layout. Here, as it's a mini-layout with types Text and Link. Since we already have an actual layout-component called Text, this might look confusing to app developers.

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()),
),
).setUnionType('discriminated'),
),
).exportAs('ColumnConfig'),
),
),
)
.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()),
).exportAs('DataConfig'),
),
);
14 changes: 14 additions & 0 deletions src/layout/SimpleTable/index.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLElement, PropsFromGenericComponent<'SimpleTable'>>(
function LayoutComponentSimpleTableRender(props, _): JSX.Element | null {
return <SimpleTableComponent {...props} />;
},
);
}
Loading