torch-glare 2.4.4 → 2.5.0

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 (147) hide show
  1. package/apps/lib/components/DataViews/{badgeAdapter.ts → badge.ts} +2 -2
  2. package/apps/lib/components/DataViews/cell.tsx +324 -0
  3. package/apps/lib/components/DataViews/context.ts +144 -0
  4. package/apps/lib/components/DataViews/data-views.tsx +383 -0
  5. package/apps/lib/components/DataViews/filters/children.tsx +98 -0
  6. package/apps/lib/components/DataViews/filters/custom.tsx +34 -0
  7. package/apps/lib/components/DataViews/filters/filters.tsx +163 -0
  8. package/apps/lib/components/DataViews/filters/index.ts +4 -0
  9. package/apps/lib/components/DataViews/filters/labelled.tsx +20 -0
  10. package/apps/lib/components/DataViews/filters/presets.tsx +65 -0
  11. package/apps/lib/components/DataViews/filters/summary.tsx +65 -0
  12. package/apps/lib/components/DataViews/filters/sync.tsx +35 -0
  13. package/apps/lib/components/DataViews/filters/values.ts +173 -0
  14. package/apps/lib/components/DataViews/header.tsx +217 -0
  15. package/apps/lib/components/DataViews/hooks/index.ts +5 -0
  16. package/apps/lib/components/DataViews/hooks/useActiveRow.ts +22 -0
  17. package/apps/lib/components/DataViews/hooks/useControllable.ts +52 -0
  18. package/apps/lib/components/DataViews/index.ts +74 -26
  19. package/apps/lib/components/DataViews/panel/columns.tsx +153 -0
  20. package/apps/lib/components/DataViews/panel/controls.tsx +106 -0
  21. package/apps/lib/components/DataViews/panel/index.ts +3 -0
  22. package/apps/lib/components/DataViews/panel/panel.tsx +164 -0
  23. package/apps/lib/components/DataViews/panel/saved-views.tsx +67 -0
  24. package/apps/lib/components/DataViews/panel/section.tsx +79 -0
  25. package/apps/lib/components/DataViews/panel/sort.tsx +42 -0
  26. package/apps/lib/components/DataViews/panel/tab.tsx +31 -0
  27. package/apps/lib/components/DataViews/slots.ts +63 -0
  28. package/apps/lib/components/DataViews/states.tsx +38 -0
  29. package/apps/lib/components/DataViews/types.ts +485 -178
  30. package/apps/lib/components/DataViews/views/board-view.tsx +379 -0
  31. package/apps/lib/components/DataViews/views/card-rows.tsx +36 -0
  32. package/apps/lib/components/DataViews/views/inbox-view.tsx +257 -0
  33. package/apps/lib/components/DataViews/views/pane-views.tsx +192 -0
  34. package/apps/lib/components/DataViews/views/table-view.tsx +426 -0
  35. package/apps/lib/components/DataViews/views/tree-view.tsx +365 -0
  36. package/apps/lib/components/FormBuilder/context.ts +20 -6
  37. package/apps/lib/components/FormBuilder/field-kind.ts +28 -0
  38. package/apps/lib/components/FormBuilder/fields/DateField.tsx +3 -3
  39. package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +7 -6
  40. package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +30 -4
  41. package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
  42. package/apps/lib/components/FormBuilder/fields/TableField.tsx +80 -52
  43. package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
  44. package/apps/lib/components/FormBuilder/form-builder.tsx +66 -6
  45. package/apps/lib/components/FormBuilder/index.ts +3 -1
  46. package/apps/lib/components/FormBuilder/types.ts +40 -0
  47. package/apps/lib/components/Input.tsx +3 -0
  48. package/apps/lib/components/SearchableTable.tsx +5 -4
  49. package/apps/lib/components/SectionBlock.tsx +58 -11
  50. package/apps/lib/components/Select.tsx +3 -1
  51. package/apps/lib/components/TabSwitch.tsx +16 -4
  52. package/apps/lib/components/Table.tsx +265 -67
  53. package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
  54. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
  55. package/apps/lib/components/TreeFolder/index.ts +1 -1
  56. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
  57. package/apps/lib/hooks/useDragDrop.tsx +365 -0
  58. package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
  59. package/apps/lib/registry.json +159 -4
  60. package/apps/lib/tsconfig.tsbuildinfo +1 -1
  61. package/apps/lib/utils/dataViews/path.ts +67 -0
  62. package/apps/lib/utils/dataViews/query.ts +73 -0
  63. package/apps/lib/utils/dataViews/types.ts +187 -0
  64. package/docs/components/breadcrumb.md +1 -1
  65. package/docs/components/button-group.md +1 -1
  66. package/docs/components/button.md +1 -1
  67. package/docs/components/card.md +1 -1
  68. package/docs/components/checkbox.md +1 -1
  69. package/docs/components/data-views/backend-response.md +324 -0
  70. package/docs/components/data-views/examples/a11y-rtl.md +250 -0
  71. package/docs/components/data-views/examples/api-orders-route.md +130 -0
  72. package/docs/components/data-views/examples/fields.md +362 -0
  73. package/docs/components/data-views/examples/filters.md +308 -0
  74. package/docs/components/data-views/examples/inbox-routing.md +218 -0
  75. package/docs/components/data-views/examples/index.md +29 -0
  76. package/docs/components/data-views/examples/overview.md +244 -0
  77. package/docs/components/data-views/examples/panel.md +212 -0
  78. package/docs/components/data-views/examples/scale.md +231 -0
  79. package/docs/components/data-views/examples/server-side.md +210 -0
  80. package/docs/components/data-views/examples/state.md +250 -0
  81. package/docs/components/data-views/examples/tree-custom.md +388 -0
  82. package/docs/components/data-views/examples/view-registry.md +313 -0
  83. package/docs/components/data-views/examples/views.md +534 -0
  84. package/docs/components/data-views/guide.md +405 -0
  85. package/docs/components/data-views/index.md +1504 -0
  86. package/docs/components/data-views/migration.md +79 -0
  87. package/docs/components/date-picker.md +0 -1
  88. package/docs/components/form-builder.md +19 -8
  89. package/docs/components/form-renderer.md +2 -1
  90. package/docs/components/form.md +1 -1
  91. package/docs/components/input-field.md +1 -1
  92. package/docs/components/input-otp.md +1 -1
  93. package/docs/components/input.md +1 -1
  94. package/docs/components/labeled-check-box.md +1 -1
  95. package/docs/components/labeled-radio.md +1 -1
  96. package/docs/components/radio-card.md +1 -1
  97. package/docs/components/radio.md +1 -1
  98. package/docs/components/search-field.md +1 -1
  99. package/docs/components/section-block.md +79 -3
  100. package/docs/components/select.md +1 -1
  101. package/docs/components/simple-select.md +1 -1
  102. package/docs/components/switch.md +1 -1
  103. package/docs/components/tab-switch.md +1 -1
  104. package/docs/components/table.md +45 -8
  105. package/docs/components/text-editor.md +1 -1
  106. package/docs/components/textarea.md +1 -1
  107. package/docs/components/toggle-button.md +1 -1
  108. package/docs/components/toggle.md +1 -1
  109. package/docs/components/tree-folder.md +110 -0
  110. package/docs/how-to/forms-with-form-builder.md +6 -4
  111. package/docs/reference/components.md +16 -6
  112. package/docs/tutorials/component-composition.md +11 -13
  113. package/package.json +3 -2
  114. package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
  115. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
  116. package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
  117. package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
  118. package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
  119. package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
  120. package/apps/lib/components/DataViews/InboxView.tsx +0 -463
  121. package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
  122. package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
  123. package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
  124. package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
  125. package/apps/lib/components/DataViews/TableView.tsx +0 -212
  126. package/apps/lib/components/DataViews/TreeView.tsx +0 -364
  127. package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
  128. package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
  129. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
  130. package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
  131. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
  132. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
  133. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
  134. package/apps/lib/hooks/useDataViewsState.ts +0 -175
  135. package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
  136. package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
  137. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
  138. package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
  139. package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
  140. package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
  141. package/docs/components/data-views-config-panel.md +0 -208
  142. package/docs/components/data-views-layout.md +0 -291
  143. package/docs/components/inbox-view.md +0 -170
  144. package/docs/components/kanban-view.md +0 -135
  145. package/docs/components/table-view.md +0 -141
  146. package/docs/components/tree-view.md +0 -147
  147. package/docs/how-to/data-views-from-backend-response.md +0 -194
@@ -1,165 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useMemo, useState } from "react";
4
- import * as Slider from "@radix-ui/react-slider";
5
- import { cn } from "../../../utils/cn";
6
- import type { FieldConfig, NumericRangeFilter } from "../types";
7
- import type { NumericExtremes } from "../../../utils/dataViews/rangeUtils";
8
-
9
- type Props = {
10
- field: FieldConfig;
11
- extremes: NumericExtremes;
12
- step: number;
13
- value: NumericRangeFilter | undefined;
14
- onChange: (next: NumericRangeFilter) => void;
15
- };
16
-
17
- export function RangeSliderWithInputs({ field, extremes, step, value, onChange }: Props) {
18
- const min = field.rangeMin ?? extremes.min;
19
- const max = field.rangeMax ?? extremes.max;
20
-
21
- const lo = value?.min ?? min;
22
- const hi = value?.max ?? max;
23
-
24
- const [loInput, setLoInput] = useState(formatForInput(lo, field));
25
- const [hiInput, setHiInput] = useState(formatForInput(hi, field));
26
-
27
- useEffect(() => {
28
- setLoInput(formatForInput(lo, field));
29
- }, [lo, field]);
30
- useEffect(() => {
31
- setHiInput(formatForInput(hi, field));
32
- }, [hi, field]);
33
-
34
- const commit = (rawLo: number, rawHi: number) => {
35
- let nlo = clamp(rawLo, min, max);
36
- let nhi = clamp(rawHi, min, max);
37
- if (nlo > nhi) [nlo, nhi] = [nhi, nlo];
38
- const atFloor = nlo === min;
39
- const atCeil = nhi === max;
40
- onChange({
41
- kind: "number",
42
- min: atFloor ? undefined : nlo,
43
- max: atCeil ? undefined : nhi,
44
- });
45
- };
46
-
47
- const handleSliderChange = (vals: number[]) => {
48
- if (vals.length !== 2) return;
49
- commit(vals[0], vals[1]);
50
- };
51
-
52
- const commitFromInputs = () => {
53
- const nlo = parseFloat(loInput);
54
- const nhi = parseFloat(hiInput);
55
- commit(Number.isFinite(nlo) ? nlo : lo, Number.isFinite(nhi) ? nhi : hi);
56
- };
57
-
58
- const prefix = useMemo(() => prefixFor(field), [field]);
59
- const suffix = useMemo(() => suffixFor(field), [field]);
60
-
61
- return (
62
- <div className="space-y-3">
63
- <Slider.Root
64
- className="relative flex items-center select-none touch-none w-full h-5"
65
- min={min}
66
- max={max}
67
- step={step}
68
- value={[lo, hi]}
69
- onValueChange={handleSliderChange}
70
- minStepsBetweenThumbs={0}
71
- >
72
- <Slider.Track className="bg-background-presentation-form-field-primary relative grow rounded-full h-1">
73
- <Slider.Range className="absolute bg-content-presentation-action-primary rounded-full h-full" />
74
- </Slider.Track>
75
- <Slider.Thumb
76
- aria-label="Minimum"
77
- className="block w-4 h-4 bg-white border-2 border-content-presentation-action-primary rounded-full shadow hover:bg-background-presentation-body-overlay-primary focus:outline-none focus:ring-2 focus:ring-content-presentation-action-primary"
78
- />
79
- <Slider.Thumb
80
- aria-label="Maximum"
81
- className="block w-4 h-4 bg-white border-2 border-content-presentation-action-primary rounded-full shadow hover:bg-background-presentation-body-overlay-primary focus:outline-none focus:ring-2 focus:ring-content-presentation-action-primary"
82
- />
83
- </Slider.Root>
84
-
85
- <div className="flex items-center gap-2">
86
- <NumberCell
87
- label="Min"
88
- value={loInput}
89
- onChange={setLoInput}
90
- onCommit={commitFromInputs}
91
- prefix={prefix}
92
- suffix={suffix}
93
- />
94
- <span className="text-xs text-content-presentation-global-tertiary">–</span>
95
- <NumberCell
96
- label="Max"
97
- value={hiInput}
98
- onChange={setHiInput}
99
- onCommit={commitFromInputs}
100
- prefix={prefix}
101
- suffix={suffix}
102
- />
103
- </div>
104
- </div>
105
- );
106
- }
107
-
108
- function NumberCell({
109
- label,
110
- value,
111
- onChange,
112
- onCommit,
113
- prefix,
114
- suffix,
115
- }: {
116
- label: string;
117
- value: string;
118
- onChange: (v: string) => void;
119
- onCommit: () => void;
120
- prefix?: string;
121
- suffix?: string;
122
- }) {
123
- return (
124
- <label className="flex-1 flex items-center gap-1 text-xs bg-background-presentation-body-primary border border-border-presentation-global-primary rounded-md px-2 py-1.5 focus-within:border-content-presentation-action-primary">
125
- <span className="sr-only">{label}</span>
126
- {prefix && <span className="text-content-presentation-global-tertiary">{prefix}</span>}
127
- <input
128
- inputMode="decimal"
129
- value={value}
130
- onChange={(e) => onChange(e.target.value)}
131
- onBlur={onCommit}
132
- onKeyDown={(e) => {
133
- if (e.key === "Enter") (e.currentTarget as HTMLInputElement).blur();
134
- }}
135
- className={cn(
136
- "w-full bg-transparent outline-none tabular-nums text-content-presentation-global-primary",
137
- )}
138
- />
139
- {suffix && <span className="text-content-presentation-global-tertiary">{suffix}</span>}
140
- </label>
141
- );
142
- }
143
-
144
- function clamp(n: number, min: number, max: number): number {
145
- return Math.min(max, Math.max(min, n));
146
- }
147
-
148
- function formatForInput(n: number, field: FieldConfig): string {
149
- if (!Number.isFinite(n)) return "";
150
- if (field.type === "star-rating") return n.toFixed(1);
151
- if (Number.isInteger(n)) return String(n);
152
- return String(n);
153
- }
154
-
155
- function prefixFor(field: FieldConfig): string | undefined {
156
- if (field.type !== "currency") return undefined;
157
- if (typeof field.currency === "string") return "$";
158
- return field.currency?.symbol ?? "$";
159
- }
160
-
161
- function suffixFor(field: FieldConfig): string | undefined {
162
- if (field.type === "progress-bar") return "%";
163
- if (field.type === "star-rating") return "★";
164
- return undefined;
165
- }
@@ -1,50 +0,0 @@
1
- "use client";
2
-
3
- import { Drawer } from "vaul";
4
- import { Menu, X } from "lucide-react";
5
- import type { ReactNode } from "react";
6
-
7
- type Props = {
8
- open: boolean;
9
- onOpenChange: (open: boolean) => void;
10
- children: ReactNode;
11
- };
12
-
13
- export function TreeDrawer({ open, onOpenChange, children }: Props) {
14
- return (
15
- <Drawer.Root open={open} onOpenChange={onOpenChange} direction="left">
16
- <Drawer.Portal>
17
- <Drawer.Overlay className="fixed inset-0 bg-black/40 z-40" />
18
- <Drawer.Content className="fixed inset-y-0 left-0 z-50 w-72 bg-background-presentation-body-overlay-primary border-r border-border-presentation-global-primary outline-none flex flex-col">
19
- <div className="flex items-center justify-between p-3 border-b border-border-presentation-global-primary">
20
- <Drawer.Title className="text-sm font-semibold text-content-presentation-global-primary">
21
- Tree
22
- </Drawer.Title>
23
- <button
24
- type="button"
25
- onClick={() => onOpenChange(false)}
26
- aria-label="Close tree"
27
- className="p-1 rounded text-content-presentation-global-tertiary hover:text-content-presentation-global-primary"
28
- >
29
- <X className="w-4 h-4" />
30
- </button>
31
- </div>
32
- <div className="flex-1 overflow-y-auto p-2">{children}</div>
33
- </Drawer.Content>
34
- </Drawer.Portal>
35
- </Drawer.Root>
36
- );
37
- }
38
-
39
- export function TreeDrawerTrigger({ onClick }: { onClick: () => void }) {
40
- return (
41
- <button
42
- type="button"
43
- onClick={onClick}
44
- aria-label="Open tree"
45
- className="inline-flex items-center justify-center h-8 w-8 rounded-md border border-border-presentation-global-primary text-content-presentation-global-secondary hover:text-content-presentation-global-primary"
46
- >
47
- <Menu className="w-4 h-4" />
48
- </button>
49
- );
50
- }
@@ -1,74 +0,0 @@
1
- "use client";
2
-
3
- import { useMemo } from "react";
4
- import type { FieldConfig } from "../types";
5
- import type { TreeNode } from "../../../utils/dataViews/treeUtils";
6
- import { getByPath } from "../../../utils/dataViews/pathUtils";
7
- import { TreeFolder } from "../../TreeFolder";
8
- import type { TreeFolderMoveArgs, TreeFolderNode } from "../../TreeFolder";
9
-
10
- type Props = {
11
- roots: TreeNode[];
12
- expanded: Set<string>;
13
- selectedId: string | null;
14
- labelField: FieldConfig;
15
- dndEnabled: boolean;
16
- onToggle: (id: string) => void;
17
- onSelect: (id: string) => void;
18
- onMove?: (args: TreeFolderMoveArgs) => void;
19
- };
20
-
21
- function toFolderNode(node: TreeNode, labelField: FieldConfig): TreeFolderNode {
22
- const labelValue = getByPath(node.record, labelField.path);
23
- const name = labelValue == null ? node.id : String(labelValue);
24
- const children = node.children.length
25
- ? node.children.map((c) => toFolderNode(c, labelField))
26
- : undefined;
27
- return {
28
- id: node.id,
29
- name,
30
- type: children ? "folder" : "file",
31
- data: node.record,
32
- children,
33
- };
34
- }
35
-
36
- export function TreeSidebar({
37
- roots,
38
- expanded,
39
- selectedId,
40
- labelField,
41
- dndEnabled,
42
- onToggle,
43
- onSelect,
44
- onMove,
45
- }: Props) {
46
- const folderData = useMemo(
47
- () => roots.map((r) => toFolderNode(r, labelField)),
48
- [roots, labelField],
49
- );
50
-
51
- const expandedIds = useMemo(() => Array.from(expanded), [expanded]);
52
-
53
- return (
54
- <TreeFolder
55
- data={folderData}
56
- selectedId={selectedId}
57
- onSelectionChange={(id) => {
58
- if (id) onSelect(id);
59
- }}
60
- expandedIds={expandedIds}
61
- onExpandedChange={(next) => {
62
- const before = new Set(expandedIds);
63
- const after = new Set(next);
64
- for (const id of after) if (!before.has(id)) onToggle(id);
65
- for (const id of before) if (!after.has(id)) onToggle(id);
66
- }}
67
- dndEnabled={dndEnabled}
68
- onMove={onMove}
69
- showHeader={false}
70
- showBreadcrumb={true}
71
- highlightAncestors={true}
72
- />
73
- );
74
- }
@@ -1,175 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useMemo, useState } from "react";
4
- import type {
5
- DynamicRecord,
6
- DynamicColumnConfig,
7
- FieldConfig,
8
- FilterState,
9
- TreeConfig,
10
- ViewConfig,
11
- ViewType,
12
- ViewVisibility,
13
- } from "../components/DataViews/types";
14
- import { defaultConfig } from "../components/DataViews/types";
15
- import { detectColumns, mergeColumns } from "../utils/dataViews/columnUtils";
16
- import { detectFields, mergeFields } from "../utils/dataViews/fieldUtils";
17
- import { autoDetectTreeShape, flattenAll } from "../utils/dataViews/treeUtils";
18
-
19
- export type UseDataViewsStateOptions = {
20
- data?: DynamicRecord[];
21
- fields?: FieldConfig[];
22
- columns?: Partial<DynamicColumnConfig>[];
23
- config?: Partial<ViewConfig>;
24
- treeConfig?: TreeConfig;
25
- views?: ViewVisibility;
26
- filterState?: FilterState;
27
- onFilterChange?: (filters: FilterState) => void;
28
- };
29
-
30
- export function useDataViewsState({
31
- data,
32
- fields,
33
- columns,
34
- config: initialConfig,
35
- treeConfig,
36
- views,
37
- filterState: externalFilterState,
38
- onFilterChange,
39
- }: UseDataViewsStateOptions) {
40
- const [currentView, setCurrentView] = useState<ViewType>(
41
- initialConfig?.defaultView || defaultConfig.defaultView,
42
- );
43
- const [config, setConfig] = useState<ViewConfig>({ ...defaultConfig, ...initialConfig });
44
- const [items, setItems] = useState<DynamicRecord[]>(data || []);
45
- const [internalFilterState, setInternalFilterState] = useState<FilterState>({});
46
-
47
- useEffect(() => {
48
- setItems(data || []);
49
- }, [data]);
50
-
51
- const activeFilterState = externalFilterState ?? internalFilterState;
52
-
53
- const treeShape = useMemo(
54
- () => autoDetectTreeShape(items, treeConfig ?? {}),
55
- [items, treeConfig],
56
- );
57
- const treeAutoAvailable = !!treeConfig || !!treeShape.childrenField || !!treeShape.parentField;
58
-
59
- const enabledViews = useMemo<Record<ViewType, boolean>>(() => {
60
- const v = views ?? {};
61
- return {
62
- table: v.table ?? true,
63
- kanban: v.kanban ?? true,
64
- inbox: v.inbox ?? true,
65
- tree: v.tree ?? treeAutoAvailable,
66
- };
67
- }, [views, treeAutoAvailable]);
68
-
69
- useEffect(() => {
70
- if (!enabledViews[currentView]) {
71
- const fallback = (Object.entries(enabledViews) as Array<[ViewType, boolean]>).find(
72
- ([, on]) => on,
73
- )?.[0];
74
- if (fallback) setCurrentView(fallback);
75
- }
76
- }, [enabledViews, currentView]);
77
-
78
- const flatItems = useMemo<DynamicRecord[]>(() => {
79
- const cf = treeConfig?.childrenField ?? treeShape.childrenField;
80
- if (!cf) return items;
81
- return flattenAll(items, cf);
82
- }, [items, treeConfig?.childrenField, treeShape.childrenField]);
83
-
84
- const detectedFields = useMemo<FieldConfig[]>(() => {
85
- if (!flatItems || flatItems.length === 0) return [];
86
- const detected = detectFields(flatItems);
87
- return mergeFields(detected, fields);
88
- }, [flatItems, fields]);
89
-
90
- const resolvedFields = useMemo<FieldConfig[]>(() => {
91
- if (detectedFields.length === 0) return detectedFields;
92
- const overrides = new Map(config.tableColumns.map((c) => [c.id, c]));
93
- return detectedFields
94
- .map((f) => {
95
- const o = overrides.get(f.path);
96
- if (!o) return f;
97
- return { ...f, visible: o.visible, order: o.order };
98
- })
99
- .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
100
- }, [detectedFields, config.tableColumns]);
101
-
102
- const detectedColumns = useMemo<DynamicColumnConfig[]>(() => {
103
- if (!flatItems || flatItems.length === 0) return [];
104
- const detected = detectColumns(flatItems);
105
- return mergeColumns(detected, columns);
106
- }, [flatItems, columns]);
107
-
108
- useEffect(() => {
109
- if (detectedFields.length === 0) return;
110
- setConfig((prev) => {
111
- const prevByPath = new Map(prev.tableColumns.map((c) => [c.id, c]));
112
- const next: typeof prev.tableColumns = [];
113
- let order = 0;
114
- for (const f of detectedFields) {
115
- if (f.type === "hidden") continue;
116
- const existing = prevByPath.get(f.path);
117
- next.push({
118
- id: f.path,
119
- label: f.label ?? f.path,
120
- visible: existing?.visible ?? f.visible !== false,
121
- order: existing?.order ?? order,
122
- });
123
- order++;
124
- prevByPath.delete(f.path);
125
- }
126
- const stale = Array.from(prevByPath.values());
127
- const carried = [...next, ...stale].slice(0, 100);
128
- if (
129
- carried.length === prev.tableColumns.length &&
130
- carried.every((c, i) => {
131
- const p = prev.tableColumns[i];
132
- return (
133
- p &&
134
- p.id === c.id &&
135
- p.label === c.label &&
136
- p.visible === c.visible &&
137
- p.order === c.order
138
- );
139
- })
140
- ) {
141
- return prev;
142
- }
143
- return { ...prev, tableColumns: carried };
144
- });
145
- }, [detectedFields]);
146
-
147
- const handleConfigChange = (newConfig: Partial<ViewConfig>) => {
148
- setConfig((prev) => ({ ...prev, ...newConfig }));
149
- };
150
-
151
- const handleDataUpdate = (updatedData: DynamicRecord[]) => {
152
- setItems(updatedData);
153
- };
154
-
155
- const handleFilterChange = (newFilters: FilterState) => {
156
- if (onFilterChange) onFilterChange(newFilters);
157
- else setInternalFilterState(newFilters);
158
- };
159
-
160
- return {
161
- items,
162
- flatItems,
163
- resolvedFields,
164
- detectedColumns,
165
- config,
166
- setConfig: handleConfigChange,
167
- currentView,
168
- setCurrentView,
169
- filterState: activeFilterState,
170
- setFilterState: handleFilterChange,
171
- onDataUpdate: handleDataUpdate,
172
- treeShape,
173
- enabledViews,
174
- };
175
- }
@@ -1,132 +0,0 @@
1
- import type { DynamicColumnConfig, DynamicRecord } from "../../components/DataViews/types";
2
- import { isPlainObject } from "./nestedDataUtils";
3
-
4
- export function detectColumns(data: DynamicRecord[]): DynamicColumnConfig[] {
5
- if (!data || data.length === 0) {
6
- return [];
7
- }
8
-
9
- const allKeys = new Set<string>();
10
- data.forEach((item) => {
11
- Object.keys(item).forEach((key) => allKeys.add(key));
12
- });
13
-
14
- const columns: DynamicColumnConfig[] = [];
15
- let order = 0;
16
-
17
- allKeys.forEach((key) => {
18
- if (key.startsWith("_")) return;
19
-
20
- const firstValue = data.find((item) => item[key] != null)?.[key];
21
-
22
- if (isPlainObject(firstValue)) return;
23
- if (Array.isArray(firstValue) && firstValue.length > 0 && isPlainObject(firstValue[0])) return;
24
-
25
- const type = inferColumnType(key, firstValue);
26
-
27
- columns.push({
28
- id: key,
29
- label: formatLabel(key),
30
- visible: true,
31
- order: order++,
32
- type,
33
- });
34
- });
35
-
36
- return columns
37
- .sort((a, b) => {
38
- if (a.id === "id") return -1;
39
- if (b.id === "id") return 1;
40
- return a.label.localeCompare(b.label);
41
- })
42
- .map((col, idx) => ({ ...col, order: idx }));
43
- }
44
-
45
- function inferColumnType(
46
- key: string,
47
- value: unknown,
48
- ): "text" | "number" | "date" | "badge" | "array" | "boolean" {
49
- if (key.toLowerCase().includes("date") || key.toLowerCase().includes("time")) {
50
- return "date";
51
- }
52
- if (key.toLowerCase().includes("status") || key.toLowerCase().includes("priority")) {
53
- return "badge";
54
- }
55
- if (key.toLowerCase().includes("tag") || key.toLowerCase().includes("label")) {
56
- return "array";
57
- }
58
-
59
- if (value === null || value === undefined) {
60
- return "text";
61
- }
62
-
63
- if (typeof value === "boolean") {
64
- return "boolean";
65
- }
66
-
67
- if (typeof value === "number") {
68
- return "number";
69
- }
70
-
71
- if (Array.isArray(value)) {
72
- return "array";
73
- }
74
-
75
- if (typeof value === "string") {
76
- if (!isNaN(Date.parse(value)) && /^\d{4}-\d{2}-\d{2}/.test(value)) {
77
- return "date";
78
- }
79
- return "text";
80
- }
81
-
82
- return "text";
83
- }
84
-
85
- function formatLabel(key: string): string {
86
- if (key.includes("_")) {
87
- return key
88
- .split("_")
89
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
90
- .join(" ");
91
- }
92
-
93
- return key
94
- .replace(/([A-Z])/g, " $1")
95
- .replace(/^./, (str) => str.toUpperCase())
96
- .trim();
97
- }
98
-
99
- export function mergeColumns(
100
- detected: DynamicColumnConfig[],
101
- custom?: Partial<DynamicColumnConfig>[],
102
- ): DynamicColumnConfig[] {
103
- if (!custom || custom.length === 0) {
104
- return detected;
105
- }
106
-
107
- const merged = [...detected];
108
- const customMap = new Map(custom.map((col) => [col.id, col]));
109
-
110
- merged.forEach((col, idx) => {
111
- const customCol = customMap.get(col.id);
112
- if (customCol) {
113
- merged[idx] = { ...col, ...customCol };
114
- customMap.delete(col.id);
115
- }
116
- });
117
-
118
- customMap.forEach((customCol) => {
119
- if (customCol.id) {
120
- merged.push({
121
- id: customCol.id,
122
- label: customCol.label || formatLabel(customCol.id),
123
- visible: customCol.visible ?? true,
124
- order: customCol.order ?? merged.length,
125
- type: customCol.type || "text",
126
- render: customCol.render,
127
- });
128
- }
129
- });
130
-
131
- return merged.sort((a, b) => a.order - b.order);
132
- }