@tsed/react-formio 3.0.0-rc.13 → 3.0.0-rc.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/all.js +2 -0
- package/dist/all.js.map +1 -1
- package/dist/chunks/index2.js +19748 -22277
- package/dist/chunks/index2.js.map +1 -1
- package/dist/chunks/moment.js +2535 -0
- package/dist/chunks/moment.js.map +1 -0
- package/dist/molecules/forms/select/Select.interface.d.ts +0 -4
- package/dist/molecules/table/all.js +2 -0
- package/dist/molecules/table/all.js.map +1 -1
- package/dist/molecules/table/components/DefaultBooleanCell.d.ts +2 -0
- package/dist/molecules/table/components/DefaultBooleanCell.js +12 -0
- package/dist/molecules/table/components/DefaultBooleanCell.js.map +1 -0
- package/dist/molecules/table/components/DefaultCell.js +7 -7
- package/dist/molecules/table/components/DefaultCell.js.map +1 -1
- package/dist/molecules/table/components/DefaultDateCell.d.ts +2 -0
- package/dist/molecules/table/components/DefaultDateCell.js +16 -0
- package/dist/molecules/table/components/DefaultDateCell.js.map +1 -0
- package/dist/molecules/table/components/DefaultFilter.d.ts +5 -7
- package/dist/molecules/table/components/DefaultFilter.js +8 -8
- package/dist/molecules/table/components/DefaultFilter.js.map +1 -1
- package/dist/molecules/table/interfaces/extends.d.ts +3 -0
- package/dist/molecules/table/utils/mapFormToColumns.d.ts +5 -1
- package/dist/molecules/table/utils/mapFormToColumns.js +54 -26
- package/dist/molecules/table/utils/mapFormToColumns.js.map +1 -1
- package/dist/organisms/table/forms/components/FormsCell.js +1 -1
- package/dist/organisms/table/submissions/SubmissionsTable.js +1 -1
- package/dist/organisms/table/submissions/SubmissionsTable.js.map +1 -1
- package/package.json +3 -3
- package/src/all.ts +2 -0
- package/src/molecules/forms/select/Select.interface.ts +0 -4
- package/src/molecules/table/Table.stories.tsx +72 -66
- package/src/molecules/table/all.ts +2 -0
- package/src/molecules/table/components/DefaultBooleanCell.spec.tsx +42 -0
- package/src/molecules/table/components/DefaultBooleanCell.tsx +11 -0
- package/src/molecules/table/components/DefaultCell.tsx +1 -1
- package/src/molecules/table/components/DefaultDateCell.spec.tsx +43 -0
- package/src/molecules/table/components/DefaultDateCell.tsx +23 -0
- package/src/molecules/table/components/DefaultFilter.tsx +10 -7
- package/src/molecules/table/filters/Filters.d.ts +1 -0
- package/src/molecules/table/interfaces/extends.ts +3 -0
- package/src/molecules/table/utils/mapFormToColumns.spec.tsx +85 -4
- package/src/molecules/table/utils/mapFormToColumns.tsx +66 -18
- package/src/organisms/table/submissions/SubmissionsTable.tsx +1 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
|
|
3
|
+
import { DefaultDateCell } from "./DefaultDateCell";
|
|
4
|
+
|
|
5
|
+
function createCellContext(value: string | undefined, format?: string) {
|
|
6
|
+
return {
|
|
7
|
+
getValue: () => value,
|
|
8
|
+
column: {
|
|
9
|
+
columnDef: {
|
|
10
|
+
meta: {
|
|
11
|
+
format
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
} as any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("DefaultDateCell", () => {
|
|
19
|
+
it("should render an empty span when value is undefined", () => {
|
|
20
|
+
render(<DefaultDateCell {...createCellContext(undefined)} />);
|
|
21
|
+
|
|
22
|
+
expect(screen.getByText("", { selector: "span" })).toBeInTheDocument();
|
|
23
|
+
expect(screen.queryByText(/.+/)).not.toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should render the formatted date with the default format", () => {
|
|
27
|
+
render(<DefaultDateCell {...createCellContext("2026-03-12T10:30:00.000Z")} />);
|
|
28
|
+
|
|
29
|
+
expect(screen.getByText("03/12/2026", { selector: "span" })).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should render the formatted date with a custom format", () => {
|
|
33
|
+
render(<DefaultDateCell {...createCellContext("2026-03-12T10:30:00.000Z", "YYYY-MM-DD HH:mm")} />);
|
|
34
|
+
|
|
35
|
+
expect(screen.getByText("2026-03-12 10:30", { selector: "span" })).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should fallback to the raw value when the date is invalid", () => {
|
|
39
|
+
render(<DefaultDateCell {...createCellContext("not-a-date")} />);
|
|
40
|
+
|
|
41
|
+
expect(screen.getByText("not-a-date", { selector: "span" })).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { CellContext } from "@tanstack/react-table";
|
|
2
|
+
import moment from "moment";
|
|
3
|
+
|
|
4
|
+
import { registerComponent } from "../../../registries/components";
|
|
5
|
+
|
|
6
|
+
export function DefaultDateCell<Data extends object>({ getValue, column: { columnDef } }: CellContext<Data, string>) {
|
|
7
|
+
const value = getValue();
|
|
8
|
+
|
|
9
|
+
if (!value) {
|
|
10
|
+
return <span />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const date = moment.parseZone(value, moment.ISO_8601, true);
|
|
14
|
+
|
|
15
|
+
if (!date.isValid()) {
|
|
16
|
+
return <span>{String(value)}</span>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return <span>{date.format(columnDef.meta?.format || "L")}</span>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
registerComponent("Cell.date", DefaultDateCell);
|
|
23
|
+
registerComponent("Cell.datetime", DefaultDateCell);
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import "../interfaces/extends";
|
|
2
2
|
|
|
3
|
-
import { Header } from "@tanstack/react-table";
|
|
3
|
+
import { Header, RowData } from "@tanstack/react-table";
|
|
4
4
|
import type { ComponentType } from "react";
|
|
5
5
|
|
|
6
6
|
import { getComponent, registerComponent } from "../../../registries/components";
|
|
7
7
|
|
|
8
|
-
export interface DefaultFilterProps<Data = any> {
|
|
9
|
-
header: Header<Data,
|
|
8
|
+
export interface DefaultFilterProps<Data extends RowData = any, TValue = unknown> {
|
|
9
|
+
header: Header<Data, TValue>;
|
|
10
10
|
i18n?: (f: string) => string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export interface FilterProps<Data = any, Opts = Record<string, unknown>> {
|
|
14
|
-
header: Header<Data, unknown>;
|
|
13
|
+
export interface FilterProps<Data extends RowData = any, Opts = Record<string, unknown>> extends DefaultFilterProps<Data> {
|
|
15
14
|
options: Opts;
|
|
16
|
-
i18n?: (f: string) => string;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
export function DefaultFilter<Data = any
|
|
17
|
+
export function DefaultFilter<Data extends RowData = any, TValue = unknown>(props: DefaultFilterProps<Data, TValue>) {
|
|
18
|
+
const { header, i18n } = props;
|
|
20
19
|
const {
|
|
21
20
|
filter = {
|
|
22
21
|
variant: "text"
|
|
@@ -32,6 +31,10 @@ export function DefaultFilter<Data = any>({ header, i18n }: DefaultFilterProps<D
|
|
|
32
31
|
return null;
|
|
33
32
|
}
|
|
34
33
|
|
|
34
|
+
if (filter.disabled) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
35
38
|
return (
|
|
36
39
|
<div className='table-cell-header__filter'>
|
|
37
40
|
<Filter header={header} options={filter} i18n={i18n} />
|
|
@@ -7,6 +7,9 @@ declare module "@tanstack/react-table" {
|
|
|
7
7
|
//allows us to define custom properties for our columns
|
|
8
8
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
9
9
|
interface ColumnMeta<TData extends RowData, TValue> {
|
|
10
|
+
type?: "string" | "number" | "boolean" | "date" | string;
|
|
11
|
+
format?: string;
|
|
12
|
+
labels?: Record<string, string>;
|
|
10
13
|
filter?: FilterOptions;
|
|
11
14
|
sort?: string;
|
|
12
15
|
cellProps?: TdHTMLAttributes<HTMLTableCellElement>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { DefaultCellBoolean } from "../components/DefaultBooleanCell";
|
|
1
2
|
import { DefaultCell } from "../components/DefaultCell";
|
|
3
|
+
import { DefaultDateCell } from "../components/DefaultDateCell";
|
|
2
4
|
import { mapFormToColumns } from "./mapFormToColumns";
|
|
3
5
|
|
|
4
6
|
describe("mapFormToColumns", () => {
|
|
5
|
-
it("should use
|
|
7
|
+
it("should use DefaultCellBoolean for mapped checkbox table columns", () => {
|
|
6
8
|
const form = {
|
|
7
9
|
components: [
|
|
8
10
|
{
|
|
@@ -14,12 +16,13 @@ describe("mapFormToColumns", () => {
|
|
|
14
16
|
]
|
|
15
17
|
} as any;
|
|
16
18
|
|
|
17
|
-
const [column] = mapFormToColumns(form) as any[];
|
|
19
|
+
const [column] = mapFormToColumns({ form: form }) as any[];
|
|
18
20
|
|
|
19
21
|
expect(column.accessorKey).toEqual("data.enabled");
|
|
20
22
|
expect(column.header).toEqual("Enabled");
|
|
23
|
+
expect(column.meta.type).toEqual("boolean");
|
|
21
24
|
expect(column.meta.filter.variant).toEqual("boolean");
|
|
22
|
-
expect(column.cell).toBe(
|
|
25
|
+
expect(column.cell).toBe(DefaultCellBoolean);
|
|
23
26
|
});
|
|
24
27
|
|
|
25
28
|
it("should fallback to DefaultCell for kept columns without a cell renderer", () => {
|
|
@@ -27,9 +30,87 @@ describe("mapFormToColumns", () => {
|
|
|
27
30
|
components: []
|
|
28
31
|
} as any;
|
|
29
32
|
|
|
30
|
-
const [column] = mapFormToColumns(form, [{ accessorKey: "data.other", header: "Other" } as any]) as any[];
|
|
33
|
+
const [column] = mapFormToColumns({ form: form, columns: [{ accessorKey: "data.other", header: "Other" } as any] }) as any[];
|
|
31
34
|
|
|
32
35
|
expect(column.accessorKey).toEqual("data.other");
|
|
33
36
|
expect(column.cell).toBe(DefaultCell);
|
|
34
37
|
});
|
|
38
|
+
|
|
39
|
+
it("should merge a kept column matched by component key", () => {
|
|
40
|
+
const form = {
|
|
41
|
+
components: [
|
|
42
|
+
{
|
|
43
|
+
type: "textfield",
|
|
44
|
+
key: "name",
|
|
45
|
+
label: "Name:",
|
|
46
|
+
tableView: true
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
} as any;
|
|
50
|
+
|
|
51
|
+
const [column] = mapFormToColumns({
|
|
52
|
+
form: form,
|
|
53
|
+
columns: [
|
|
54
|
+
{
|
|
55
|
+
id: "name",
|
|
56
|
+
header: "Custom name",
|
|
57
|
+
meta: {
|
|
58
|
+
order: 5,
|
|
59
|
+
filter: {
|
|
60
|
+
variant: "text"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} as any
|
|
64
|
+
]
|
|
65
|
+
}) as any[];
|
|
66
|
+
|
|
67
|
+
expect(column.accessorKey).toEqual("data.name");
|
|
68
|
+
expect(column.header).toEqual("Custom name");
|
|
69
|
+
expect(column.meta.order).toEqual(5);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should use DefaultDateCell for mapped datetime columns", () => {
|
|
73
|
+
const form = {
|
|
74
|
+
components: [
|
|
75
|
+
{
|
|
76
|
+
type: "datetime",
|
|
77
|
+
key: "createdAt",
|
|
78
|
+
label: "Created at:",
|
|
79
|
+
tableView: true
|
|
80
|
+
}
|
|
81
|
+
]
|
|
82
|
+
} as any;
|
|
83
|
+
|
|
84
|
+
const [column] = mapFormToColumns({ form: form }) as any[];
|
|
85
|
+
|
|
86
|
+
expect(column.accessorKey).toEqual("data.createdAt");
|
|
87
|
+
expect(column.meta.type).toEqual("date");
|
|
88
|
+
expect(column.cell).toBe(DefaultDateCell);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should dedupe columns when a kept column matches a mapped accessor key", () => {
|
|
92
|
+
const form = {
|
|
93
|
+
components: [
|
|
94
|
+
{
|
|
95
|
+
type: "textfield",
|
|
96
|
+
key: "email",
|
|
97
|
+
label: "Email:",
|
|
98
|
+
tableView: true
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
} as any;
|
|
102
|
+
|
|
103
|
+
const columns = mapFormToColumns({
|
|
104
|
+
form: form,
|
|
105
|
+
columns: [
|
|
106
|
+
{
|
|
107
|
+
accessorKey: "data.email",
|
|
108
|
+
header: "Email"
|
|
109
|
+
} as any
|
|
110
|
+
]
|
|
111
|
+
}) as any[];
|
|
112
|
+
|
|
113
|
+
expect(columns).toHaveLength(1);
|
|
114
|
+
expect(columns[0].accessorKey).toEqual("data.email");
|
|
115
|
+
});
|
|
35
116
|
});
|
|
@@ -9,18 +9,44 @@ import { getComponent } from "../../../registries/components";
|
|
|
9
9
|
import type { DefaultCell } from "../components/DefaultCell";
|
|
10
10
|
import type { FilterVariants } from "../filters/Filters.js";
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const MAP_FILTER_TYPES: Record<string, FilterVariants> = {
|
|
13
13
|
number: "range",
|
|
14
14
|
currency: "range",
|
|
15
15
|
checkbox: "boolean"
|
|
16
|
-
};
|
|
16
|
+
} as const;
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const MAP_TYPES = {
|
|
19
|
+
date: "date",
|
|
20
|
+
datetime: "date",
|
|
21
|
+
number: "number",
|
|
22
|
+
currency: "currency",
|
|
23
|
+
checkbox: "boolean"
|
|
24
|
+
} as const;
|
|
25
|
+
|
|
26
|
+
function getColumnIdentity<Data>(column: ColumnDef<Data, any> | ColumnDefResolved<Data, any>) {
|
|
27
|
+
if ("id" in column && typeof column.id === "string") {
|
|
28
|
+
return column.id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if ("accessorKey" in column && typeof column.accessorKey === "string") {
|
|
32
|
+
return column.accessorKey;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function mapFormToColumns<Data = any>({
|
|
39
|
+
form,
|
|
40
|
+
columns = [],
|
|
41
|
+
prefix = "data."
|
|
42
|
+
}: {
|
|
43
|
+
form: FormType;
|
|
44
|
+
columns?: ColumnDefResolved<Data, any>[];
|
|
45
|
+
prefix?: string;
|
|
46
|
+
}): ColumnDef<Data, any>[] {
|
|
19
47
|
const columnHelper = createColumnHelper<Data>();
|
|
20
48
|
const columnsToKeep = cloneDeep(columns);
|
|
21
49
|
|
|
22
|
-
const Cell = getComponent<typeof DefaultCell>("Cell");
|
|
23
|
-
|
|
24
50
|
const columnsFromComponents = form.components
|
|
25
51
|
.flatMap((component) => {
|
|
26
52
|
if (component.type === "tabs") {
|
|
@@ -32,9 +58,13 @@ export function mapFormToColumns<Data = any>(form: FormType, columns: ColumnDefR
|
|
|
32
58
|
.filter((component) => component?.tableView)
|
|
33
59
|
.map((c) => {
|
|
34
60
|
const component = c as ComponentType;
|
|
61
|
+
const componentColumnKey = `${prefix}${component.key}`;
|
|
62
|
+
const matchingKeys = new Set([component.key, componentColumnKey]);
|
|
63
|
+
|
|
64
|
+
const columnIndex = columnsToKeep.findIndex((column) => {
|
|
65
|
+
const identity = getColumnIdentity(column);
|
|
35
66
|
|
|
36
|
-
|
|
37
|
-
return accessorKey === `data.${component.key}`;
|
|
67
|
+
return identity ? matchingKeys.has(identity) : false;
|
|
38
68
|
});
|
|
39
69
|
|
|
40
70
|
let column = columnsToKeep[columnIndex];
|
|
@@ -43,25 +73,43 @@ export function mapFormToColumns<Data = any>(form: FormType, columns: ColumnDefR
|
|
|
43
73
|
columnsToKeep.splice(columnIndex, 1);
|
|
44
74
|
}
|
|
45
75
|
|
|
46
|
-
return columnHelper.accessor(
|
|
76
|
+
return columnHelper.accessor(componentColumnKey as any, {
|
|
47
77
|
header: (component.label || component.title || component.key)?.replace(/:/, ""),
|
|
48
|
-
cell: Cell,
|
|
49
78
|
meta: {
|
|
50
|
-
|
|
79
|
+
type: (MAP_TYPES[component.type as keyof typeof MAP_TYPES] || component.type) as any,
|
|
80
|
+
filter: {
|
|
81
|
+
...column?.meta?.filter,
|
|
82
|
+
variant: MAP_FILTER_TYPES[component.type as keyof typeof MAP_FILTER_TYPES] || "text"
|
|
83
|
+
},
|
|
51
84
|
...(column?.meta || {})
|
|
52
85
|
},
|
|
53
86
|
...(column || {})
|
|
54
87
|
});
|
|
55
88
|
});
|
|
56
89
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
const dedupedColumns = [...columnsFromComponents, ...(columnsToKeep as any[])].reduce<ColumnDef<Data, any>[]>((acc, column) => {
|
|
91
|
+
const identity = getColumnIdentity(column);
|
|
92
|
+
|
|
93
|
+
if (identity && acc.some((existingColumn) => getColumnIdentity(existingColumn) === identity)) {
|
|
94
|
+
return acc;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
acc.push(column);
|
|
98
|
+
return acc;
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
const mergedColumns = dedupedColumns.map((column, index) => {
|
|
102
|
+
const Cell = getComponent<typeof DefaultCell>([`Cell.${column.id}`, `Cell.${column.meta?.type}`, "Cell"]);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
...column,
|
|
106
|
+
meta: {
|
|
107
|
+
...column?.meta,
|
|
108
|
+
order: get(column, "meta.order", index * 10)
|
|
109
|
+
},
|
|
110
|
+
cell: column.cell || Cell
|
|
111
|
+
};
|
|
112
|
+
});
|
|
65
113
|
|
|
66
114
|
return mergedColumns.sort((a, b) => (a.meta.order > b.meta.order ? 1 : -1)) as ColumnDef<Data, any>[];
|
|
67
115
|
}
|
|
@@ -8,7 +8,7 @@ export type SubmissionsTableProps<Data extends object = JSONRecord> = Omit<Table
|
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
export function SubmissionsTable<Data extends object = JSONRecord>({ form, ...props }: SubmissionsTableProps<Data>) {
|
|
11
|
-
const columns: any[] | undefined = form && mapFormToColumns(form);
|
|
11
|
+
const columns: any[] | undefined = form && mapFormToColumns({ form: form });
|
|
12
12
|
|
|
13
13
|
return <Table {...(props as any)} columns={columns!} />;
|
|
14
14
|
}
|