Skip to content

Commit

Permalink
[Result-list] Formatting of attributes (#241, #280)
Browse files Browse the repository at this point in the history
  • Loading branch information
SvenReissig committed Feb 21, 2024
1 parent ba6ee78 commit ca6c2fd
Show file tree
Hide file tree
Showing 17 changed files with 459 additions and 35 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-masks-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@open-pioneer/result-list": minor
---

Added formatting support of result list table cells depending on the type of value including support of cell render functions.
7 changes: 7 additions & 0 deletions .changeset/friendly-hats-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@open-pioneer/result-list": patch
"@open-pioneer/legend": patch
"@open-pioneer/toc": patch
---

Don't remove uppercase characters in slugs
1 change: 1 addition & 0 deletions src/packages/legend/Legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ function useLegendAttributes(layer: LayerBase) {

function slug(id: string) {
return id
.toLowerCase()
.replace(/[^a-z0-9 -]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-");
Expand Down
6 changes: 4 additions & 2 deletions src/packages/result-list/DataTable/createColumns.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ it("expect createColumn to create columns correctly", async () => {
const resultListColumns = createResultListColumns();

// Slice away the selection checkbox column
const columns = createColumns(resultListColumns, intl).slice(1);
const columns = createColumns({ columns: resultListColumns, intl: intl }).slice(1);
expect(columns.length).toEqual(resultListColumns.length);
const [simplePropColumn, colWithDisplayName, colWithWidth, colWithGetter] = columns;

Expand All @@ -39,7 +39,9 @@ it("expect createColumn to distribute remaining width on columns with undefined
const metaData = dummyMetaDataMissingWidth;
const fullWidth = 1000;
// Slice away the selection checkbox column
const columns = createColumns(metaData, intl, fullWidth).slice(1);
const columns = createColumns({ columns: metaData, intl: intl, tableWidth: fullWidth }).slice(
1
);
const expectedWidth = (fullWidth - SELECT_COLUMN_SIZE - 300) / 2;
expect(columns[0]?.size).toEqual(metaData[0]!.width);
expect(columns[1]?.size).toEqual(expectedWidth);
Expand Down
75 changes: 67 additions & 8 deletions src/packages/result-list/DataTable/createColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,50 @@ import { BaseFeature } from "@open-pioneer/map";
import { PackageIntl } from "@open-pioneer/runtime";
import { createColumnHelper } from "@tanstack/react-table";
import { Table as TanstackTable } from "@tanstack/table-core/build/lib/types";
import { FormatOptions, ResultColumn } from "../ResultList";
import { SelectCheckbox } from "./SelectCheckbox";
import { ResultColumn } from "../ResultList";

export const SELECT_COLUMN_SIZE = 70;

const columnHelper = createColumnHelper<BaseFeature>();

export function createColumns(columns: ResultColumn[], intl: PackageIntl, tableWidth?: number) {
export interface CreateColumnsOptions {
columns: ResultColumn[];
intl: PackageIntl;
tableWidth?: number;
formatOptions?: FormatOptions;
}

export function createColumns(options: CreateColumnsOptions) {
const { columns, intl, tableWidth, formatOptions } = options;
const remainingColumnWidth: number | undefined =
tableWidth === undefined ? undefined : calcRemainingColumnWidth(columns, tableWidth);
const selectionColumn = createSelectionColumn(intl);
const columnDefs = columns.map((column, index) => {
const columnWidth = column.width || remainingColumnWidth;
const configuredId =
column.id ?? (column.propertyName && slug(column.propertyName)) ?? String(index);
return createColumn(column, columnWidth, "result-list-col_" + configuredId);
column.id || (column.propertyName && slug(column.propertyName)) || String(index);
return createColumn({
id: "result-list-col_" + configuredId,
column: column,
intl: intl,
columnWidth: columnWidth,
formatOptions: formatOptions
});
});
return [selectionColumn, ...columnDefs];
}

function createColumn(column: ResultColumn, columnWidth: number | undefined, id: string) {
interface CreateColumnOptions {
id: string;
column: ResultColumn;
intl: PackageIntl;
columnWidth?: number;
formatOptions?: FormatOptions;
}

function createColumn(options: CreateColumnOptions) {
const { id, column, columnWidth, formatOptions, intl } = options;
const { propertyName, getPropertyValue } = column;
const hasPropertyValue = getPropertyValue != null || propertyName != null;

Expand All @@ -43,17 +67,51 @@ function createColumn(column: ResultColumn, columnWidth: number | undefined, id:
id: id,
cell: (info) => {
const cellValue = info.getValue();
if (cellValue == null) {
return "";
if (column.renderCell) {
return column.renderCell({
feature: info.row.original,
value: cellValue
});
}
return String(cellValue);
return renderFunc(cellValue, intl, formatOptions);
},
header: column.displayName ?? column.propertyName,
size: columnWidth
}
);
}

function renderFunc(cellValue: unknown, intl: PackageIntl, formatOptions?: FormatOptions) {
if (cellValue === null || cellValue === undefined) return "";
const type = typeof cellValue;
const formatNumber = (num: number | bigint) => {
if (Number.isNaN(num)) return "";
return intl.formatNumber(num, formatOptions?.numberOptions);
};

switch (type) {
case "number": {
return formatNumber(cellValue as number);
}
case "bigint": {
return formatNumber(cellValue as bigint);
}
case "boolean": {
return intl.formatMessage({ id: `displayBoolean.${cellValue}` });
}
case "string": {
return cellValue as string;
}
case "object": {
if (cellValue instanceof Date)
return intl.formatDate(cellValue, formatOptions?.dateOptions);
return cellValue.toString();
}
default:
return String(cellValue);
}
}

function createSelectionColumn(intl: PackageIntl) {
return columnHelper.display({
id: "selection-buttons",
Expand Down Expand Up @@ -127,6 +185,7 @@ function getCheckboxToolTip<Data>(table: TanstackTable<Data>, intl: PackageIntl)

function slug(id: string) {
return id
.toLowerCase()
.replace(/[^a-z0-9 -]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-");
Expand Down
28 changes: 25 additions & 3 deletions src/packages/result-list/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,25 @@ import { ResultList } from "@open-pioneer/result-list";

See below for how to assemble the `input` parameter.

### Configuring result list data and columns
### Configuring result list data, columns and formatOptions

The `input` prop determines which features are displayed (`input.data`) and in what format (`input.columns`).
The `input` prop determines which features are displayed (`input.data`) and in what format (`input.columns`
and `input.formatOptions`).
`input` must conform to the TypeScript interface `ResultListInput`.

`input.data` must be an array of features (TypeScript interface `BaseFeature`).
Features can be defined manually (they are rather simple objects), but they can also be obtained from other package's UI Components,
for example from `@open-pioneer/search` and `@open-pioneer/selection`.

`input.columns` is an array of column definitions (TypeScript interface `ResultColumn`).
These columns define which properties of the configured features are shown.
These columns define which properties of the configured features are shown, or how cells of the column
should be rendered.
The `ResultList` will render the specified columns in the order in which they are given.

`input.formatOptions` is being used to specify how numbers and dates are formatted. You can provide
`numberOptions` and `dateOptions` and these settings are applied for all table cells that
have no `render` function configured and matches the corresponding type.

Consider a set of features which all have the properties `name` and `age`.
In that case, a simple configuration of the `ResultList` may look as follows:

Expand Down Expand Up @@ -79,6 +85,22 @@ const columns = [
]
```

If you want to display a cell value as a very customizable react component, you can provide a `renderCell` function to each column:

```tsx
// Simple usage of a render function
// The `renderCell` function is called for every feature.
// It should be efficient because it can be invoked many times.
const columns = [
{
displayName: "ID",
renderCell: ({ feature }) => (
<chakra.div>{`This item has the following ID: ${feature.id}`}</chakra.div>
)
}
];
```

### Selection

The user can select (and deselect) individual features by clicking on the checkbox at the beginning of a row.
Expand Down
123 changes: 115 additions & 8 deletions src/packages/result-list/ResultList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,17 @@ it("expect all rows to be selected and deselected", async () => {
selectRowSelects.forEach((checkbox) => expect(checkbox.checked).toBeFalsy());
});

it("expect result list display all data types", async () => {
it("expect result list display all data types except dates", async () => {
render(
<PackageContextProvider>
<PackageContextProvider locale="de">
<ResultList
input={{ data: dummyFeatureData, columns: dummyColumns }}
input={{
data: dummyFeatureData,
columns: dummyColumns,
formatOptions: {
numberOptions: { maximumFractionDigits: 3 }
}
}}
mapId="foo"
data-testid="result-list"
/>
Expand All @@ -237,15 +243,15 @@ it("expect result list display all data types", async () => {
const firstRowCells = Array.from(allRows[0]!.querySelectorAll("td"));
expect(firstRowCells).toHaveLength(6);

const [selectCell, stringCell, integerCell, floatCell, trueCell, ..._rest] = firstRowCells;
const [selectCell, stringCell, integerCell, floatCell, trueCell] = firstRowCells;
expect(selectCell!.innerHTML).includes("<input");
expect(stringCell!.textContent).toBe("Test");
expect(integerCell!.textContent).toBe("123");
expect(floatCell!.textContent).toBe("4.567");
expect(trueCell!.textContent).toBe("true");
expect(floatCell!.textContent).toBe("4,567");
expect(trueCell!.textContent).toBe("displayBoolean.true");

const falseCell = allRows[1]?.querySelectorAll("td")[4];
expect(falseCell!.textContent).toBe("false"); // false is not rendered as ""
expect(falseCell!.textContent).toBe("displayBoolean.false");

// Null / Undefined is rendered as an empty string
const lastRowCells = Array.from(allRows[3]!.querySelectorAll("td"));
Expand All @@ -256,6 +262,67 @@ it("expect result list display all data types", async () => {
}
});

it("expect result list display date in given format", async () => {
const dateTimeFormatOptions: Intl.DateTimeFormatOptions = {
dateStyle: "medium",
timeStyle: "medium",
timeZone: "UTC"
};

let dateFormatter = Intl.DateTimeFormat("de-DE", dateTimeFormatOptions);
const resultListComp = (
<ResultList
input={{
data: dummyDateFeatureData,
columns: dummyDateColumns,
formatOptions: {
numberOptions: { maximumFractionDigits: 3 },
dateOptions: dateTimeFormatOptions
}
}}
mapId="foo"
data-testid="result-list"
/>
);

const renderResult = render(
<PackageContextProvider locale="de">{resultListComp}</PackageContextProvider>
);

const { allRows } = await waitForResultList();
const firstRowCells = Array.from(allRows[0]!.querySelectorAll("td"));
const [_, dateCell] = firstRowCells;
expect(dateCell!.textContent).toBe(dateFormatter.format(new Date("2020-05-12T23:50:21.817Z")));

renderResult.rerender(
<PackageContextProvider locale="en">{resultListComp}</PackageContextProvider>
);
await waitForResultList(); // TODO: Workaround to hide react warning due to useEffect (use disableReactWarning helper after printing merge)

dateFormatter = Intl.DateTimeFormat("en-US", dateTimeFormatOptions);
expect(dateCell!.textContent).toBe(dateFormatter.format(new Date("2020-05-12T23:50:21.817Z")));
});

it("expect render function to be applied", async () => {
render(
<PackageContextProvider locale="de">
<ResultList
input={{
data: dummyDateFeatureData,
columns: dummyColumnsWithRenderFunc
}}
mapId="foo"
data-testid="result-list"
/>
</PackageContextProvider>
);

const { allRows } = await waitForResultList();
const firstRowCells = Array.from(allRows[0]!.querySelectorAll("td"));
const [_, dateCell] = firstRowCells;
expect(dateCell!.textContent).toMatchSnapshot();
});

async function waitForResultList() {
return await waitFor(async () => {
const resultListDiv: HTMLDivElement | null =
Expand Down Expand Up @@ -298,13 +365,42 @@ function formatDate(date: Date) {
return DATE_FORMAT.format(date);
}

const dummyDateFeatureData: BaseFeature[] = [
{
id: "1",
properties: {
"a": new Date("2020-05-12T23:50:21.817Z")
},
geometry: undefined
}
];

const dummyDateColumns: ResultColumn[] = [
{
propertyName: "a",
displayName: "Spalte A",
width: 100
}
];

const dummyColumnsWithRenderFunc: ResultColumn[] = [
{
propertyName: "a",
displayName: "Spalte A",
width: 100,
renderCell: ({ feature }) => (
<div className="renderTest">{`This item has the following ID: ${feature.id}`}</div>
)
}
];

const dummyFeatureData: BaseFeature[] = [
{
id: "1",
properties: {
"a": "Test",
"b": 123,
"c": 4.567,
"c": 4.5671365,
"d": true,
"e": formatDate(new Date("2020-05-12T23:50:21.817Z"))
},
Expand Down Expand Up @@ -342,6 +438,17 @@ const dummyFeatureData: BaseFeature[] = [
"e": undefined
},
geometry: new Point([406590.87, 5758311.82])
},
{
id: "5",
properties: {
"a": NaN,
"b": NaN,
"c": NaN,
"d": NaN,
"e": NaN
},
geometry: undefined
}
];

Expand Down
Loading

0 comments on commit ca6c2fd

Please sign in to comment.