@tsed/react-formio 3.0.0-rc.2 → 3.0.0-rc.20

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 (109) hide show
  1. package/dist/all.js +2 -0
  2. package/dist/all.js.map +1 -1
  3. package/dist/chunks/_baseGet.js +102 -0
  4. package/dist/chunks/_baseGet.js.map +1 -0
  5. package/dist/chunks/_baseSlice.js +12 -65
  6. package/dist/chunks/_baseSlice.js.map +1 -1
  7. package/dist/chunks/camelCase.js +1 -1
  8. package/dist/chunks/get.js +16 -0
  9. package/dist/chunks/get.js.map +1 -0
  10. package/dist/chunks/index2.js +19746 -22285
  11. package/dist/chunks/index2.js.map +1 -1
  12. package/dist/chunks/moment.js +2535 -0
  13. package/dist/chunks/moment.js.map +1 -0
  14. package/dist/chunks/omit.js +173 -268
  15. package/dist/chunks/omit.js.map +1 -1
  16. package/dist/chunks/toString.js +56 -0
  17. package/dist/chunks/toString.js.map +1 -0
  18. package/dist/interfaces/JSONRecord.d.ts +4 -0
  19. package/dist/interfaces/JSONRecord.js +2 -0
  20. package/dist/interfaces/JSONRecord.js.map +1 -0
  21. package/dist/interfaces/Operation.d.ts +3 -12
  22. package/dist/interfaces/SubmissionType.d.ts +4 -14
  23. package/dist/interfaces/index.d.ts +0 -1
  24. package/dist/molecules/forms/select/Select.interface.d.ts +0 -4
  25. package/dist/molecules/table/Table.d.ts +4 -12
  26. package/dist/molecules/table/Table.js +34 -33
  27. package/dist/molecules/table/Table.js.map +1 -1
  28. package/dist/molecules/table/all.js +2 -0
  29. package/dist/molecules/table/all.js.map +1 -1
  30. package/dist/molecules/table/components/DefaultBooleanCell.d.ts +2 -0
  31. package/dist/molecules/table/components/DefaultBooleanCell.js +12 -0
  32. package/dist/molecules/table/components/DefaultBooleanCell.js.map +1 -0
  33. package/dist/molecules/table/components/DefaultCell.d.ts +2 -5
  34. package/dist/molecules/table/components/DefaultCell.js +8 -6
  35. package/dist/molecules/table/components/DefaultCell.js.map +1 -1
  36. package/dist/molecules/table/components/DefaultCellOperations.d.ts +4 -11
  37. package/dist/molecules/table/components/DefaultCellOperations.js.map +1 -1
  38. package/dist/molecules/table/components/DefaultDateCell.d.ts +2 -0
  39. package/dist/molecules/table/components/DefaultDateCell.js +16 -0
  40. package/dist/molecules/table/components/DefaultDateCell.js.map +1 -0
  41. package/dist/molecules/table/components/DefaultFilter.d.ts +5 -7
  42. package/dist/molecules/table/components/DefaultFilter.js +8 -8
  43. package/dist/molecules/table/components/DefaultFilter.js.map +1 -1
  44. package/dist/molecules/table/components/DefaultOperationButton.d.ts +4 -11
  45. package/dist/molecules/table/components/DefaultOperationButton.js.map +1 -1
  46. package/dist/molecules/table/filters/SelectFilter.js +22 -20
  47. package/dist/molecules/table/filters/SelectFilter.js.map +1 -1
  48. package/dist/molecules/table/filters/TextFieldFilter.js +16 -16
  49. package/dist/molecules/table/filters/TextFieldFilter.js.map +1 -1
  50. package/dist/molecules/table/hooks/useTable.d.ts +4 -11
  51. package/dist/molecules/table/hooks/useTable.js +14 -14
  52. package/dist/molecules/table/hooks/useTable.js.map +1 -1
  53. package/dist/molecules/table/hooks/useUniqValues.d.ts +8 -5
  54. package/dist/molecules/table/hooks/useUniqValues.js +23 -7
  55. package/dist/molecules/table/hooks/useUniqValues.js.map +1 -1
  56. package/dist/molecules/table/interfaces/extends.d.ts +3 -0
  57. package/dist/molecules/table/utils/mapFormToColumns.d.ts +5 -1
  58. package/dist/molecules/table/utils/mapFormToColumns.js +53 -26
  59. package/dist/molecules/table/utils/mapFormToColumns.js.map +1 -1
  60. package/dist/organisms/form/Form.d.ts +3 -11
  61. package/dist/organisms/form/Form.js.map +1 -1
  62. package/dist/organisms/form/actions/FormAction.js +5 -5
  63. package/dist/organisms/form/types.d.ts +1 -9
  64. package/dist/organisms/form/useForm.d.ts +4 -11
  65. package/dist/organisms/form/useForm.js.map +1 -1
  66. package/dist/organisms/table/forms/components/FormsCell.js +1 -1
  67. package/dist/organisms/table/submissions/SubmissionsTable.d.ts +4 -11
  68. package/dist/organisms/table/submissions/SubmissionsTable.js +2 -5
  69. package/dist/organisms/table/submissions/SubmissionsTable.js.map +1 -1
  70. package/dist/organisms/views/FormViews.d.ts +4 -11
  71. package/dist/organisms/views/FormViews.js.map +1 -1
  72. package/package.json +3 -3
  73. package/src/all.ts +2 -0
  74. package/src/interfaces/JSONRecord.ts +2 -0
  75. package/src/interfaces/Operation.ts +3 -6
  76. package/src/interfaces/SubmissionType.ts +4 -8
  77. package/src/interfaces/index.ts +0 -1
  78. package/src/molecules/forms/select/Select.interface.ts +0 -4
  79. package/src/molecules/table/Table.stories.tsx +101 -66
  80. package/src/molecules/table/Table.tsx +57 -56
  81. package/src/molecules/table/all.ts +2 -0
  82. package/src/molecules/table/components/DefaultBooleanCell.spec.tsx +42 -0
  83. package/src/molecules/table/components/DefaultBooleanCell.tsx +11 -0
  84. package/src/molecules/table/components/DefaultCell.spec.tsx +32 -0
  85. package/src/molecules/table/components/DefaultCell.tsx +8 -9
  86. package/src/molecules/table/components/DefaultCellOperations.tsx +4 -3
  87. package/src/molecules/table/components/DefaultDateCell.spec.tsx +43 -0
  88. package/src/molecules/table/components/DefaultDateCell.tsx +23 -0
  89. package/src/molecules/table/components/DefaultFilter.tsx +10 -7
  90. package/src/molecules/table/components/DefaultOperationButton.tsx +4 -4
  91. package/src/molecules/table/filters/Filters.d.ts +3 -1
  92. package/src/molecules/table/filters/SelectFilter.tsx +5 -3
  93. package/src/molecules/table/filters/TextFieldFilter.tsx +5 -3
  94. package/src/molecules/table/hooks/useTable.tsx +8 -13
  95. package/src/molecules/table/hooks/useUniqValues.spec.tsx +82 -0
  96. package/src/molecules/table/hooks/useUniqValues.tsx +42 -6
  97. package/src/molecules/table/interfaces/extends.ts +3 -0
  98. package/src/molecules/table/utils/mapFormToColumns.spec.tsx +116 -0
  99. package/src/molecules/table/utils/mapFormToColumns.tsx +66 -26
  100. package/src/organisms/form/Form.stories.tsx +7 -2
  101. package/src/organisms/form/Form.tsx +3 -3
  102. package/src/organisms/form/types.ts +1 -6
  103. package/src/organisms/form/useForm.ts +6 -5
  104. package/src/organisms/table/submissions/SubmissionsTable.tsx +5 -10
  105. package/src/organisms/views/FormViews.tsx +6 -9
  106. package/dist/interfaces/QueryOptions.d.ts +0 -23
  107. package/dist/interfaces/QueryOptions.js +0 -2
  108. package/dist/interfaces/QueryOptions.js.map +0 -1
  109. package/src/interfaces/QueryOptions.ts +0 -24
@@ -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} />
@@ -2,13 +2,13 @@ import { CellContext } from "@tanstack/react-table";
2
2
  import cx from "classnames";
3
3
  import { HTMLAttributes } from "react";
4
4
 
5
- import { type CellMetadata, type JSON, Operation } from "../../../interfaces";
5
+ import { type CellMetadata, Operation } from "../../../interfaces";
6
+ import type { JSONRecord } from "../../../interfaces/JSONRecord.js";
6
7
  import { registerComponent } from "../../../registries/components";
7
8
  import { iconClass } from "../../../utils/iconClass";
8
9
  import { stopPropagationWrapper } from "../../../utils/stopPropagationWrapper";
9
10
 
10
- export interface OperationButtonProps<Data extends { [key: string]: JSON } = { [key: string]: JSON }>
11
- extends Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> {
11
+ export interface OperationButtonProps<Data extends object = JSONRecord> extends Omit<HTMLAttributes<HTMLButtonElement>, "onClick"> {
12
12
  operation: Operation<Data>;
13
13
  info: CellContext<Data, unknown>;
14
14
  metadata?: CellMetadata;
@@ -16,7 +16,7 @@ export interface OperationButtonProps<Data extends { [key: string]: JSON } = { [
16
16
  i18n?: (i18n: string) => string;
17
17
  }
18
18
 
19
- export function DefaultOperationButton<Data extends { [key: string]: JSON } = { [key: string]: JSON }>(props: OperationButtonProps<Data>) {
19
+ export function DefaultOperationButton<Data extends object = JSONRecord>(props: OperationButtonProps<Data>) {
20
20
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
21
21
  const { i18n = (f: string) => f, onClick, operation, info, ...extraProps } = props;
22
22
  const { className = "btn", buttonSize = "xs", buttonType = "primary", buttonOutline, action, title = "", icon = "" } = operation;
@@ -1,3 +1,4 @@
1
+ import type { FilterProps } from "../../../../molecules/table/components/DefaultFilter";
1
2
  import { SelectOptionProps, SelectProps } from "../../forms/select/Select.interface";
2
3
 
3
4
  export type FilterVariants = "text" | "range" | "select" | "boolean";
@@ -8,6 +9,7 @@ export interface FilterBaseOptions extends Record<string, unknown> {
8
9
 
9
10
  export interface FilterTextOptions extends FilterBaseOptions {
10
11
  variant: "text";
12
+ disabled?: boolean;
11
13
  disableDatalist?: boolean;
12
14
  }
13
15
 
@@ -20,7 +22,7 @@ export interface FilterRangeOptions extends FilterBaseOptions {
20
22
 
21
23
  export interface FilterSelectOptions extends FilterBaseOptions, SelectProps<string> {
22
24
  variant: "select";
23
- options?: SelectOptionProps<string>[];
25
+ options?: SelectOptionProps<string>[] | ((props: FilterProps<any, FilterSelectOptions>) => SelectOptionProps<string>[]);
24
26
  }
25
27
 
26
28
  export interface FilterBooleanOptions extends FilterBaseOptions {
@@ -8,7 +8,7 @@ import { FilterSelectOptions } from "./Filters";
8
8
  export function SelectFilter<Data = any>({ header, options }: FilterProps<Data, FilterSelectOptions>) {
9
9
  const Select = getComponent<typeof DefaultSelect>("Select");
10
10
  const columnFilterValue = header.column.getFilterValue();
11
- const uniqValues = useUniqValues<Data>({ header, filterVariant: "text" });
11
+ const uniqValues = useUniqValues<Data>({ header, options });
12
12
 
13
13
  const opts =
14
14
  options.layout === "choicesjs"
@@ -26,7 +26,7 @@ export function SelectFilter<Data = any>({ header, options }: FilterProps<Data,
26
26
  value: ""
27
27
  }
28
28
  ] as SelectOptionProps[]
29
- ).concat(options.options || uniqValues.map((value: any) => ({ label: value, value })));
29
+ ).concat(uniqValues);
30
30
 
31
31
  return (
32
32
  <>
@@ -34,10 +34,11 @@ export function SelectFilter<Data = any>({ header, options }: FilterProps<Data,
34
34
  size='small'
35
35
  {...(options as any)}
36
36
  {...opts}
37
+ autoComplete='off'
37
38
  options={listOptions}
38
39
  name={`filter_${header.column.id}`}
39
40
  data-testid={`filter_${header.column.id}`}
40
- value={columnFilterValue as string}
41
+ value={(columnFilterValue ?? "") as string}
41
42
  onChange={(_, value) => header.column.setFilterValue(value)}
42
43
  />
43
44
  </>
@@ -45,3 +46,4 @@ export function SelectFilter<Data = any>({ header, options }: FilterProps<Data,
45
46
  }
46
47
 
47
48
  registerComponent("Filter.select", SelectFilter);
49
+ registerComponent("Filter.boolean", SelectFilter);
@@ -7,15 +7,17 @@ import { FilterTextOptions } from "./Filters";
7
7
  export function TextFieldFilter<Data = any>({ header, options }: FilterProps<Data, FilterTextOptions>) {
8
8
  const InputText = getComponent<typeof DefaultInputText>("InputText");
9
9
  const columnFilterValue = header.column.getFilterValue();
10
- const uniqValues = useUniqValues<Data>({ header, filterVariant: "text" });
10
+ const uniqValues = useUniqValues<Data>({ header });
11
11
  const datalistId = `data_list_${header.column.id}`;
12
12
 
13
13
  return (
14
14
  <>
15
15
  {!options.disableDatalist && (
16
16
  <datalist id={datalistId}>
17
- {uniqValues.map((value: any) => (
18
- <option value={value} key={value} />
17
+ {uniqValues.map((item) => (
18
+ <option value={item.value} key={item.value}>
19
+ {item.label || item.value}
20
+ </option>
19
21
  ))}
20
22
  </datalist>
21
23
  )}
@@ -13,12 +13,12 @@ import {
13
13
  import { useEffect } from "react";
14
14
 
15
15
  import { useI18n } from "../../../hooks/useI18n.js";
16
- import { type FormOptions, type JSON, Operation } from "../../../interfaces";
16
+ import { type FormOptions, Operation } from "../../../interfaces";
17
+ import type { JSONRecord } from "../../../interfaces/JSONRecord.js";
17
18
  import { getComponent } from "../../../registries/components";
18
19
  import type { DefaultCellOperations } from "../components/DefaultCellOperations";
19
20
 
20
- export interface UseTableProps<Data extends { [key: string]: JSON } = { [key: string]: JSON }>
21
- extends Omit<TableOptions<Data>, "getCoreRowModel" | "onClick"> {
21
+ export interface UseTableProps<Data extends object = JSONRecord> extends Omit<TableOptions<Data>, "onClick" | "getCoreRowModel"> {
22
22
  operations: Operation<Data>[];
23
23
  metadata?: Record<string, unknown>;
24
24
  i18n?: FormOptions["i18n"];
@@ -28,15 +28,10 @@ export interface UseTableProps<Data extends { [key: string]: JSON } = { [key: st
28
28
  pageSizes?: number[];
29
29
  }
30
30
 
31
- export function useTable<Data extends { [key: string]: JSON } = { [key: string]: JSON }>(props: UseTableProps<Data>) {
31
+ export function useTable<Data extends object = JSONRecord>(props: UseTableProps<Data>) {
32
32
  const Operations = getComponent<typeof DefaultCellOperations<Data>>("CellOperations");
33
33
  const { t } = useI18n(props.i18n);
34
34
 
35
- // const [pagination, setPagination] = useState({
36
- // pageIndex: 0, //initial page index
37
- // pageSize: 10 //default page size
38
- // });
39
-
40
35
  const operations = props.operations.length
41
36
  ? ([
42
37
  {
@@ -54,10 +49,10 @@ export function useTable<Data extends { [key: string]: JSON } = { [key: string]:
54
49
  columns: [...props.columns, ...operations],
55
50
  getCoreRowModel: getCoreRowModel(),
56
51
  getPaginationRowModel: getPaginationRowModel(),
57
- getFilteredRowModel: !props.manualFiltering ? getFilteredRowModel() : undefined,
58
- getSortedRowModel: !props.manualSorting ? getSortedRowModel() : undefined,
59
- getFacetedRowModel: !props.manualFaceted ? getFacetedRowModel() : undefined, // client-side faceting
60
- getFacetedUniqueValues: !props.manualFaceted ? getFacetedUniqueValues() : undefined // generate unique values for select filter/autocomplete
52
+ getFilteredRowModel: !props.manualFiltering ? props.getFilteredRowModel || getFilteredRowModel() : undefined,
53
+ getSortedRowModel: !props.manualSorting ? props.getSortedRowModel || getSortedRowModel() : undefined,
54
+ getFacetedRowModel: !props.manualFaceted ? props.getFacetedRowModel || getFacetedRowModel() : undefined, // client-side faceting
55
+ getFacetedUniqueValues: !props.manualFaceted ? props.getFacetedUniqueValues || getFacetedUniqueValues() : undefined // generate unique values for select filter/autocomplete
61
56
  });
62
57
 
63
58
  const { columnFilters, sorting, pagination, columnOrder, columnPinning, columnSizing, columnSizingInfo, columnVisibility, globalFilter } =
@@ -0,0 +1,82 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { vi } from "vitest";
3
+
4
+ import { useUniqValues } from "./useUniqValues";
5
+
6
+ function createHeader({ variant, labels, values = [] }: { variant?: string; labels?: Record<string, string>; values?: string[] }) {
7
+ return {
8
+ column: {
9
+ columnDef: {
10
+ meta: {
11
+ filter: {
12
+ variant
13
+ },
14
+ labels
15
+ }
16
+ },
17
+ getFacetedUniqueValues: () => new Map(values.map((value) => [value, 1]))
18
+ }
19
+ } as any;
20
+ }
21
+
22
+ describe("useUniqValues", () => {
23
+ it("should return boolean options with the custom labels", () => {
24
+ const header = createHeader({
25
+ variant: "boolean",
26
+ labels: {
27
+ yes: "Enabled",
28
+ No: "Disabled"
29
+ }
30
+ });
31
+
32
+ const { result } = renderHook(() => useUniqValues({ header }));
33
+
34
+ expect(result.current).toEqual([
35
+ { label: "Enabled", value: "true" },
36
+ { label: "Disabled", value: "false" }
37
+ ]);
38
+ });
39
+
40
+ it("should map faceted unique values to select options", () => {
41
+ const header = createHeader({
42
+ values: ["beta", "alpha"]
43
+ });
44
+
45
+ const { result } = renderHook(() => useUniqValues({ header }));
46
+
47
+ expect(result.current).toEqual([
48
+ { label: "alpha", value: "alpha" },
49
+ { label: "beta", value: "beta" }
50
+ ]);
51
+ });
52
+
53
+ it("should return provided select options", () => {
54
+ const header = createHeader({});
55
+ const options = [
56
+ { label: "First", value: "first" },
57
+ { label: "Second", value: "second" }
58
+ ];
59
+
60
+ const { result } = renderHook(() => useUniqValues({ header, options: { variant: "select", options } as any }));
61
+
62
+ expect(result.current).toEqual(options);
63
+ });
64
+
65
+ it("should resolve select options from callback", () => {
66
+ const header = createHeader({});
67
+ const optionsFn = vi.fn().mockReturnValue([{ label: "From callback", value: "callback" }]);
68
+
69
+ const { result } = renderHook(() =>
70
+ useUniqValues({
71
+ header,
72
+ options: { variant: "select", options: optionsFn } as any
73
+ })
74
+ );
75
+
76
+ expect(optionsFn).toHaveBeenCalledWith({
77
+ header,
78
+ options: { variant: "select", options: optionsFn }
79
+ });
80
+ expect(result.current).toEqual([{ label: "From callback", value: "callback" }]);
81
+ });
82
+ });
@@ -1,10 +1,46 @@
1
- import type { Header } from "@tanstack/react-table";
2
1
  import { useMemo } from "react";
3
2
 
4
- export function useUniqValues<Data = any>({ header, filterVariant }: { header: Header<Data, unknown>; filterVariant: string }) {
5
- return useMemo(
6
- () => (filterVariant === "range" ? [] : Array.from(header.column.getFacetedUniqueValues().keys()).flat().sort().slice(0, 5000)),
3
+ import { SelectOptionProps } from "../../../molecules/forms/select/Select.interface";
4
+ import type { FilterProps } from "../components/DefaultFilter";
5
+ import { FilterSelectOptions } from "../filters/Filters";
7
6
 
8
- [header.column, filterVariant]
9
- );
7
+ type UseUniqValuesProps<Data = any> = Omit<FilterProps<Data, FilterSelectOptions>, "options"> & {
8
+ options?: FilterSelectOptions;
9
+ };
10
+
11
+ export function useUniqValues<Data = any>({ header, options }: UseUniqValuesProps<Data>): SelectOptionProps[] {
12
+ return useMemo(() => {
13
+ const providedOptions = options?.options;
14
+
15
+ if (providedOptions) {
16
+ if (typeof providedOptions === "function") {
17
+ return providedOptions({ header, options: options as FilterSelectOptions });
18
+ }
19
+
20
+ return providedOptions;
21
+ }
22
+
23
+ switch (header.column.columnDef.meta?.filter?.variant) {
24
+ case "boolean":
25
+ return [
26
+ { label: header.column.columnDef.meta?.labels?.["yes"] || "Yes", value: "true" },
27
+ {
28
+ label: header.column.columnDef.meta?.labels?.["no"] || header.column.columnDef.meta?.labels?.["No"] || "No",
29
+ value: "false"
30
+ }
31
+ ];
32
+
33
+ default:
34
+ return Array.from(header.column.getFacetedUniqueValues().keys())
35
+ .flat()
36
+ .filter((value) => value !== undefined && value !== null)
37
+ .map((value) => String(value))
38
+ .sort()
39
+ .slice(0, 5000)
40
+ .map((value) => ({
41
+ label: value,
42
+ value
43
+ }));
44
+ }
45
+ }, [header, options]);
10
46
  }
@@ -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>;
@@ -0,0 +1,116 @@
1
+ import { DefaultCellBoolean } from "../components/DefaultBooleanCell";
2
+ import { DefaultCell } from "../components/DefaultCell";
3
+ import { DefaultDateCell } from "../components/DefaultDateCell";
4
+ import { mapFormToColumns } from "./mapFormToColumns";
5
+
6
+ describe("mapFormToColumns", () => {
7
+ it("should use DefaultCellBoolean for mapped checkbox table columns", () => {
8
+ const form = {
9
+ components: [
10
+ {
11
+ type: "checkbox",
12
+ key: "enabled",
13
+ label: "Enabled:",
14
+ tableView: true
15
+ }
16
+ ]
17
+ } as any;
18
+
19
+ const [column] = mapFormToColumns({ form: form }) as any[];
20
+
21
+ expect(column.accessorKey).toEqual("data.enabled");
22
+ expect(column.header).toEqual("Enabled");
23
+ expect(column.meta.type).toEqual("boolean");
24
+ expect(column.meta.filter.variant).toEqual("boolean");
25
+ expect(column.cell).toBe(DefaultCellBoolean);
26
+ });
27
+
28
+ it("should fallback to DefaultCell for kept columns without a cell renderer", () => {
29
+ const form = {
30
+ components: []
31
+ } as any;
32
+
33
+ const [column] = mapFormToColumns({ form: form, columns: [{ accessorKey: "data.other", header: "Other" } as any] }) as any[];
34
+
35
+ expect(column.accessorKey).toEqual("data.other");
36
+ expect(column.cell).toBe(DefaultCell);
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
+ });
116
+ });
@@ -1,6 +1,5 @@
1
1
  import "../interfaces/extends";
2
2
 
3
- import { Components } from "@formio/js";
4
3
  import { ColumnDef, ColumnDefResolved, createColumnHelper } from "@tanstack/react-table";
5
4
  import cloneDeep from "lodash/cloneDeep";
6
5
  import get from "lodash/get";
@@ -10,18 +9,44 @@ import { getComponent } from "../../../registries/components";
10
9
  import type { DefaultCell } from "../components/DefaultCell";
11
10
  import type { FilterVariants } from "../filters/Filters.js";
12
11
 
13
- const MAP_TYPES: Record<string, FilterVariants> = {
12
+ const MAP_FILTER_TYPES: Record<string, FilterVariants> = {
14
13
  number: "range",
15
14
  currency: "range",
16
15
  checkbox: "boolean"
17
- };
16
+ } as const;
18
17
 
19
- 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>[] {
20
47
  const columnHelper = createColumnHelper<Data>();
21
48
  const columnsToKeep = cloneDeep(columns);
22
49
 
23
- const Cell = getComponent<typeof DefaultCell>("Cell");
24
-
25
50
  const columnsFromComponents = form.components
26
51
  .flatMap((component) => {
27
52
  if (component.type === "tabs") {
@@ -33,10 +58,13 @@ export function mapFormToColumns<Data = any>(form: FormType, columns: ColumnDefR
33
58
  .filter((component) => component?.tableView)
34
59
  .map((c) => {
35
60
  const component = c as ComponentType;
36
- const cmp: any = Components.create(component, {}, null);
61
+ const componentColumnKey = `${prefix}${component.key}`;
62
+ const matchingKeys = new Set([component.key, componentColumnKey]);
37
63
 
38
- const columnIndex = columnsToKeep.findIndex(({ accessorKey }) => {
39
- return accessorKey === `data.${component.key}`;
64
+ const columnIndex = columnsToKeep.findIndex((column) => {
65
+ const identity = getColumnIdentity(column);
66
+
67
+ return identity ? matchingKeys.has(identity) : false;
40
68
  });
41
69
 
42
70
  let column = columnsToKeep[columnIndex];
@@ -45,31 +73,43 @@ export function mapFormToColumns<Data = any>(form: FormType, columns: ColumnDefR
45
73
  columnsToKeep.splice(columnIndex, 1);
46
74
  }
47
75
 
48
- return columnHelper.accessor(`data.${component.key}` as any, {
76
+ return columnHelper.accessor(componentColumnKey as any, {
49
77
  header: (component.label || component.title || component.key)?.replace(/:/, ""),
50
- cell: (info) => {
51
- return <Cell value={info.getValue() as Data} render={(value: Data) => cmp.asString(value)} />;
52
- },
53
78
  meta: {
54
- 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
+ },
55
84
  ...(column?.meta || {})
56
85
  },
57
86
  ...(column || {})
58
87
  });
59
88
  });
60
89
 
61
- const mergedColumns = columnsFromComponents.concat(columnsToKeep as any[]).map((column, index) => ({
62
- ...column,
63
- meta: {
64
- ...(column.meta || {}),
65
- order: get(column, "meta.order", index * 10)
66
- },
67
- cell:
68
- column.cell ||
69
- ((info) => {
70
- return <Cell value={info.getValue() as Data} render={(value: Data) => value} />;
71
- })
72
- }));
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
+ });
73
113
 
74
114
  return mergedColumns.sort((a, b) => (a.meta.order > b.meta.order ? 1 : -1)) as ColumnDef<Data, any>[];
75
115
  }
@@ -260,12 +260,17 @@ export const WithSubmissionData: Story = {
260
260
  */
261
261
  export const WithOnSubmit: Story = {
262
262
  render(args) {
263
+ type OnSubmitData = {
264
+ firstName?: string;
265
+ [key: string]: unknown;
266
+ };
267
+
263
268
  // eslint-disable-next-line react-hooks/rules-of-hooks
264
- const [data, setData] = useState(() => args.submission!.data);
269
+ const [data, setData] = useState<OnSubmitData>(() => args.submission!.data as OnSubmitData);
265
270
 
266
271
  // eslint-disable-next-line react-hooks/rules-of-hooks
267
272
  useEffect(() => {
268
- setData(args.submission!.data);
273
+ setData(args.submission!.data as OnSubmitData);
269
274
  }, [args.submission]);
270
275
 
271
276
  return (
@@ -1,9 +1,9 @@
1
- import { JSON } from "../../interfaces";
1
+ import type { JSONRecord } from "../../interfaces/JSONRecord.js";
2
2
  import { useForm, UseFormProps } from "./useForm";
3
3
 
4
4
  class CSSProperties {}
5
5
 
6
- export interface FormProps<Data extends { [key: string]: JSON } = { [key: string]: JSON }> extends UseFormProps<Data> {
6
+ export interface FormProps<Data extends object = JSONRecord> extends UseFormProps<Data> {
7
7
  ["data-testid"]?: string;
8
8
  /**
9
9
  *
@@ -13,7 +13,7 @@ export interface FormProps<Data extends { [key: string]: JSON } = { [key: string
13
13
  style?: CSSProperties;
14
14
  }
15
15
 
16
- export function Form<Data extends { [key: string]: JSON } = { [key: string]: JSON }>({
16
+ export function Form<Data extends object = JSONRecord>({
17
17
  style,
18
18
  className,
19
19
  "data-testid": dataTestId = "formio-container",
@@ -1,8 +1,3 @@
1
- import { ComponentType, JSON, SubmissionType } from "../../interfaces";
2
-
3
- export interface FormPageChangeProps<Data extends { [key: string]: JSON } = { [key: string]: JSON }> {
4
- page: number;
5
- submission: SubmissionType<Data>;
6
- }
1
+ import { ComponentType } from "../../interfaces";
7
2
 
8
3
  export type FormCustomEvent = { type: string; event: string; component: ComponentType; data: any };