@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.
Files changed (43) hide show
  1. package/dist/all.js +2 -0
  2. package/dist/all.js.map +1 -1
  3. package/dist/chunks/index2.js +19748 -22277
  4. package/dist/chunks/index2.js.map +1 -1
  5. package/dist/chunks/moment.js +2535 -0
  6. package/dist/chunks/moment.js.map +1 -0
  7. package/dist/molecules/forms/select/Select.interface.d.ts +0 -4
  8. package/dist/molecules/table/all.js +2 -0
  9. package/dist/molecules/table/all.js.map +1 -1
  10. package/dist/molecules/table/components/DefaultBooleanCell.d.ts +2 -0
  11. package/dist/molecules/table/components/DefaultBooleanCell.js +12 -0
  12. package/dist/molecules/table/components/DefaultBooleanCell.js.map +1 -0
  13. package/dist/molecules/table/components/DefaultCell.js +7 -7
  14. package/dist/molecules/table/components/DefaultCell.js.map +1 -1
  15. package/dist/molecules/table/components/DefaultDateCell.d.ts +2 -0
  16. package/dist/molecules/table/components/DefaultDateCell.js +16 -0
  17. package/dist/molecules/table/components/DefaultDateCell.js.map +1 -0
  18. package/dist/molecules/table/components/DefaultFilter.d.ts +5 -7
  19. package/dist/molecules/table/components/DefaultFilter.js +8 -8
  20. package/dist/molecules/table/components/DefaultFilter.js.map +1 -1
  21. package/dist/molecules/table/interfaces/extends.d.ts +3 -0
  22. package/dist/molecules/table/utils/mapFormToColumns.d.ts +5 -1
  23. package/dist/molecules/table/utils/mapFormToColumns.js +54 -26
  24. package/dist/molecules/table/utils/mapFormToColumns.js.map +1 -1
  25. package/dist/organisms/table/forms/components/FormsCell.js +1 -1
  26. package/dist/organisms/table/submissions/SubmissionsTable.js +1 -1
  27. package/dist/organisms/table/submissions/SubmissionsTable.js.map +1 -1
  28. package/package.json +3 -3
  29. package/src/all.ts +2 -0
  30. package/src/molecules/forms/select/Select.interface.ts +0 -4
  31. package/src/molecules/table/Table.stories.tsx +72 -66
  32. package/src/molecules/table/all.ts +2 -0
  33. package/src/molecules/table/components/DefaultBooleanCell.spec.tsx +42 -0
  34. package/src/molecules/table/components/DefaultBooleanCell.tsx +11 -0
  35. package/src/molecules/table/components/DefaultCell.tsx +1 -1
  36. package/src/molecules/table/components/DefaultDateCell.spec.tsx +43 -0
  37. package/src/molecules/table/components/DefaultDateCell.tsx +23 -0
  38. package/src/molecules/table/components/DefaultFilter.tsx +10 -7
  39. package/src/molecules/table/filters/Filters.d.ts +1 -0
  40. package/src/molecules/table/interfaces/extends.ts +3 -0
  41. package/src/molecules/table/utils/mapFormToColumns.spec.tsx +85 -4
  42. package/src/molecules/table/utils/mapFormToColumns.tsx +66 -18
  43. package/src/organisms/table/submissions/SubmissionsTable.tsx +1 -1
@@ -18,4 +18,4 @@ export function DefaultCell<Data = any>({ getValue, renderValue }: CellContext<D
18
18
  }
19
19
 
20
20
  registerComponent("Cell", DefaultCell);
21
- registerComponent("Cell.text", DefaultCell);
21
+ registerComponent("Cell.string", DefaultCell);
@@ -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, unknown>;
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>({ header, i18n }: DefaultFilterProps<Data>) {
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} />
@@ -8,6 +8,7 @@ export interface FilterBaseOptions extends Record<string, unknown> {
8
8
 
9
9
  export interface FilterTextOptions extends FilterBaseOptions {
10
10
  variant: "text";
11
+ disabled?: boolean;
11
12
  disableDatalist?: boolean;
12
13
  }
13
14
 
@@ -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 DefaultCell for mapped table columns", () => {
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(DefaultCell);
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 MAP_TYPES: Record<string, FilterVariants> = {
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
- export function mapFormToColumns<Data = any>(form: FormType, columns: ColumnDefResolved<Data, any>[] = []): ColumnDef<Data, any>[] {
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
- const columnIndex = columnsToKeep.findIndex(({ accessorKey }) => {
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(`data.${component.key}` as any, {
76
+ return columnHelper.accessor(componentColumnKey as any, {
47
77
  header: (component.label || component.title || component.key)?.replace(/:/, ""),
48
- cell: Cell,
49
78
  meta: {
50
- filter: { variant: MAP_TYPES[component.type!] || "text" },
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 mergedColumns = columnsFromComponents.concat(columnsToKeep as any[]).map((column, index) => ({
58
- ...column,
59
- meta: {
60
- ...(column.meta || {}),
61
- order: get(column, "meta.order", index * 10)
62
- },
63
- cell: column.cell || Cell
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
  }