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,234 +0,0 @@
1
- import type {
2
- DynamicRecord,
3
- FieldConfig,
4
- FieldPreset,
5
- FilterValue,
6
- NumericRangeFilter,
7
- DateRangeFilter,
8
- RangeFilter,
9
- } from "../../components/DataViews/types";
10
- import { getByPath } from "./pathUtils";
11
-
12
- export function isRangeFilter(v: unknown): v is RangeFilter {
13
- return (
14
- !!v &&
15
- typeof v === "object" &&
16
- !Array.isArray(v) &&
17
- ((v as RangeFilter).kind === "number" || (v as RangeFilter).kind === "date")
18
- );
19
- }
20
-
21
- export function isNumericRange(v: FilterValue | undefined | null): v is NumericRangeFilter {
22
- return !!v && !Array.isArray(v) && (v as RangeFilter).kind === "number";
23
- }
24
-
25
- export function isDateRange(v: FilterValue | undefined | null): v is DateRangeFilter {
26
- return !!v && !Array.isArray(v) && (v as RangeFilter).kind === "date";
27
- }
28
-
29
- export function isFilterActive(v: FilterValue | undefined): boolean {
30
- if (v == null) return false;
31
- if (Array.isArray(v)) return v.length > 0;
32
- if (isNumericRange(v)) return v.min != null || v.max != null;
33
- if (isDateRange(v)) return v.from != null || v.to != null;
34
- return false;
35
- }
36
-
37
- export function countActiveFilters(state: Record<string, FilterValue>): number {
38
- let n = 0;
39
- for (const v of Object.values(state)) {
40
- if (isFilterActive(v)) n++;
41
- }
42
- return n;
43
- }
44
-
45
- export function describeFilterValue(v: FilterValue): string {
46
- if (Array.isArray(v)) return v.join(", ");
47
- if (isNumericRange(v)) {
48
- if (v.min != null && v.max != null) return `${v.min} – ${v.max}`;
49
- if (v.min != null) return `≥ ${v.min}`;
50
- if (v.max != null) return `≤ ${v.max}`;
51
- return "any";
52
- }
53
- if (isDateRange(v)) {
54
- if (v.from && v.to) return `${v.from} → ${v.to}`;
55
- if (v.from) return `from ${v.from}`;
56
- if (v.to) return `until ${v.to}`;
57
- return "any";
58
- }
59
- return "";
60
- }
61
-
62
- export type NumericExtremes = { min: number; max: number };
63
-
64
- export function computeNumericExtremes(
65
- data: DynamicRecord[],
66
- path: string,
67
- ): NumericExtremes | null {
68
- let min = Number.POSITIVE_INFINITY;
69
- let max = Number.NEGATIVE_INFINITY;
70
- let found = false;
71
- for (const item of data) {
72
- const v = getByPath(item, path);
73
- const n = typeof v === "number" ? v : Number(v);
74
- if (!Number.isFinite(n)) continue;
75
- if (n < min) min = n;
76
- if (n > max) max = n;
77
- found = true;
78
- }
79
- return found ? { min, max } : null;
80
- }
81
-
82
- export function inferStep(field: FieldConfig, extremes: NumericExtremes): number {
83
- if (field.rangeStep != null) return field.rangeStep;
84
- switch (field.type) {
85
- case "star-rating":
86
- return 0.1;
87
- case "progress-bar":
88
- return 1;
89
- case "currency": {
90
- const span = extremes.max - extremes.min;
91
- if (span >= 10000) return 100;
92
- if (span >= 1000) return 10;
93
- return 1;
94
- }
95
- default: {
96
- const span = extremes.max - extremes.min;
97
- if (span >= 10000) return 10;
98
- if (span >= 100) return 1;
99
- return 0.1;
100
- }
101
- }
102
- }
103
-
104
- export type DateExtremes = { from: string; to: string };
105
-
106
- export function computeDateExtremes(data: DynamicRecord[], path: string): DateExtremes | null {
107
- let minMs = Number.POSITIVE_INFINITY;
108
- let maxMs = Number.NEGATIVE_INFINITY;
109
- let found = false;
110
- for (const item of data) {
111
- const v = getByPath(item, path);
112
- if (v == null) continue;
113
- const ms = v instanceof Date ? v.getTime() : Date.parse(String(v));
114
- if (!Number.isFinite(ms)) continue;
115
- if (ms < minMs) minMs = ms;
116
- if (ms > maxMs) maxMs = ms;
117
- found = true;
118
- }
119
- if (!found) return null;
120
- return { from: toIsoDate(new Date(minMs)), to: toIsoDate(new Date(maxMs)) };
121
- }
122
-
123
- const RELATIVE = /^(-?\d+)([dwmy])$/;
124
-
125
- export function resolveRelativeDate(token: string | undefined): string | undefined {
126
- if (!token) return undefined;
127
- if (token === "today" || token === "now") return toIsoDate(new Date());
128
- if (token === "start-of-year") {
129
- const now = new Date();
130
- return toIsoDate(new Date(now.getFullYear(), 0, 1));
131
- }
132
- const m = RELATIVE.exec(token);
133
- if (m) {
134
- const n = parseInt(m[1], 10);
135
- const unit = m[2];
136
- const d = new Date();
137
- switch (unit) {
138
- case "d":
139
- d.setDate(d.getDate() + n);
140
- break;
141
- case "w":
142
- d.setDate(d.getDate() + n * 7);
143
- break;
144
- case "m":
145
- d.setMonth(d.getMonth() + n);
146
- break;
147
- case "y":
148
- d.setFullYear(d.getFullYear() + n);
149
- break;
150
- }
151
- return toIsoDate(d);
152
- }
153
- const ms = Date.parse(token);
154
- if (Number.isFinite(ms)) return toIsoDate(new Date(ms));
155
- return token;
156
- }
157
-
158
- export function toIsoDate(d: Date): string {
159
- const yyyy = d.getFullYear();
160
- const mm = String(d.getMonth() + 1).padStart(2, "0");
161
- const dd = String(d.getDate()).padStart(2, "0");
162
- return `${yyyy}-${mm}-${dd}`;
163
- }
164
-
165
- export function presetToFilterValue(preset: FieldPreset): RangeFilter {
166
- if ("from" in preset || "to" in preset) {
167
- return {
168
- kind: "date",
169
- from: resolveRelativeDate(preset.from),
170
- to: resolveRelativeDate(preset.to),
171
- };
172
- }
173
- return {
174
- kind: "number",
175
- min: (preset as { min?: number }).min,
176
- max: (preset as { max?: number }).max,
177
- };
178
- }
179
-
180
- export function isPresetActive(preset: FieldPreset, current: FilterValue | undefined): boolean {
181
- if (!current) return false;
182
- if (Array.isArray(current)) return false;
183
- if (current.kind === "number" && !("from" in preset) && !("to" in preset)) {
184
- return (
185
- ((preset as { min?: number }).min ?? null) === (current.min ?? null) &&
186
- ((preset as { max?: number }).max ?? null) === (current.max ?? null)
187
- );
188
- }
189
- if (current.kind === "date" && ("from" in preset || "to" in preset)) {
190
- const want = presetToFilterValue(preset) as DateRangeFilter;
191
- return (
192
- (want.from ?? null) === (current.from ?? null) && (want.to ?? null) === (current.to ?? null)
193
- );
194
- }
195
- return false;
196
- }
197
-
198
- const DEFAULT_PRESETS: Record<string, FieldPreset[]> = {
199
- currency: [
200
- { label: "<$100", max: 100 },
201
- { label: "$100–$500", min: 100, max: 500 },
202
- { label: "$500–$2k", min: 500, max: 2000 },
203
- { label: "$2k+", min: 2000 },
204
- ],
205
- "progress-bar": [
206
- { label: "0%", min: 0, max: 0 },
207
- { label: "1–40%", min: 1, max: 40 },
208
- { label: "40–70%", min: 40, max: 70 },
209
- { label: "70–99%", min: 70, max: 99 },
210
- { label: "100%", min: 100, max: 100 },
211
- ],
212
- "star-rating": [
213
- { label: "≥4★", min: 4 },
214
- { label: "≥3★", min: 3 },
215
- { label: "<3★", max: 3 },
216
- ],
217
- date: [
218
- { label: "Today", from: "today" },
219
- { label: "Last 7 days", from: "-7d" },
220
- { label: "Last 30 days", from: "-30d" },
221
- { label: "This year", from: "start-of-year" },
222
- ],
223
- "date-format": [
224
- { label: "Today", from: "today" },
225
- { label: "Last 7 days", from: "-7d" },
226
- { label: "Last 30 days", from: "-30d" },
227
- { label: "This year", from: "start-of-year" },
228
- ],
229
- };
230
-
231
- export function resolvePresets(field: FieldConfig): FieldPreset[] {
232
- if (field.presets) return field.presets;
233
- return DEFAULT_PRESETS[field.type ?? ""] ?? [];
234
- }
@@ -1,396 +0,0 @@
1
- import type { DynamicRecord, TreeConfig } from "../../components/DataViews/types";
2
- import { getByPath, setByPath } from "./pathUtils";
3
-
4
- export type TreeNode = {
5
- id: string;
6
- record: DynamicRecord;
7
- children: TreeNode[];
8
- depth: number;
9
- parent: TreeNode | null;
10
- };
11
-
12
- export type ResolvedTreeConfig = {
13
- childrenField?: string;
14
- parentField?: string;
15
- idField: string;
16
- orderField?: string;
17
- nodeLabel?: string;
18
- defaultExpanded: "all" | "roots" | "none";
19
- };
20
-
21
- const CHILDREN_PATTERNS = ["children", "items", "kids", "subItems", "nodes"];
22
- const PARENT_PATTERNS = ["parentId", "parent_id", "parent", "managerId", "manager"];
23
- const ID_PATTERNS = ["id", "_id", "uuid", "key"];
24
-
25
- export function autoDetectTreeShape(data: DynamicRecord[], config: TreeConfig): ResolvedTreeConfig {
26
- const sample = data.find((r) => r != null && typeof r === "object") ?? {};
27
-
28
- const idField = config.idField ?? pickKey(sample, ID_PATTERNS) ?? "id";
29
-
30
- let childrenField = config.childrenField;
31
- let parentField = config.parentField;
32
-
33
- if (!childrenField && !parentField) {
34
- childrenField = pickArrayKey(sample, CHILDREN_PATTERNS);
35
- if (!childrenField) parentField = pickKey(sample, PARENT_PATTERNS);
36
- }
37
-
38
- return {
39
- childrenField,
40
- parentField,
41
- idField,
42
- orderField: config.orderField,
43
- nodeLabel: config.nodeLabel,
44
- defaultExpanded: config.defaultExpanded ?? "roots",
45
- };
46
- }
47
-
48
- function pickKey(record: DynamicRecord, candidates: string[]): string | undefined {
49
- for (const key of candidates) {
50
- if (key in record) return key;
51
- }
52
- return undefined;
53
- }
54
-
55
- function pickArrayKey(record: DynamicRecord, candidates: string[]): string | undefined {
56
- for (const key of candidates) {
57
- if (key in record && Array.isArray(record[key])) return key;
58
- }
59
- return undefined;
60
- }
61
-
62
- export function buildTree(data: DynamicRecord[], config: ResolvedTreeConfig): TreeNode[] {
63
- if (!data || data.length === 0) return [];
64
- if (config.childrenField) return buildFromNested(data, config);
65
- if (config.parentField) return buildFromFlat(data, config);
66
- return data.map((record, idx) => makeNode(record, config, idx, null, 0));
67
- }
68
-
69
- function buildFromNested(data: DynamicRecord[], config: ResolvedTreeConfig): TreeNode[] {
70
- const seen = new Set<string>();
71
-
72
- const visit = (
73
- record: DynamicRecord,
74
- depth: number,
75
- parent: TreeNode | null,
76
- idx: number,
77
- ): TreeNode => {
78
- const node = makeNode(record, config, idx, parent, depth);
79
- if (seen.has(node.id)) {
80
- return { ...node, children: [] };
81
- }
82
- seen.add(node.id);
83
-
84
- const rawChildren = getByPath(record, config.childrenField);
85
- if (Array.isArray(rawChildren)) {
86
- node.children = rawChildren.map((child, ci) => visit(child, depth + 1, node, ci));
87
- }
88
- return node;
89
- };
90
-
91
- return data.map((record, idx) => visit(record, 0, null, idx));
92
- }
93
-
94
- function buildFromFlat(data: DynamicRecord[], config: ResolvedTreeConfig): TreeNode[] {
95
- const byId = new Map<string, TreeNode>();
96
- for (let i = 0; i < data.length; i++) {
97
- const record = data[i];
98
- const id = String(getByPath(record, config.idField) ?? `__row${i}`);
99
- byId.set(id, {
100
- id,
101
- record,
102
- children: [],
103
- depth: 0,
104
- parent: null,
105
- });
106
- }
107
-
108
- const roots: TreeNode[] = [];
109
- for (const node of byId.values()) {
110
- const parentId = getByPath(node.record, config.parentField);
111
- if (parentId == null) {
112
- roots.push(node);
113
- continue;
114
- }
115
- const parent = byId.get(String(parentId));
116
- if (!parent || parent === node) {
117
- roots.push(node);
118
- continue;
119
- }
120
- node.parent = parent;
121
- parent.children.push(node);
122
- }
123
-
124
- const setDepth = (n: TreeNode, d: number, guard = new Set<TreeNode>()) => {
125
- if (guard.has(n)) return;
126
- guard.add(n);
127
- n.depth = d;
128
- for (const c of n.children) setDepth(c, d + 1, guard);
129
- };
130
- for (const r of roots) setDepth(r, 0);
131
-
132
- return roots;
133
- }
134
-
135
- function makeNode(
136
- record: DynamicRecord,
137
- config: ResolvedTreeConfig,
138
- idx: number,
139
- parent: TreeNode | null,
140
- depth: number,
141
- ): TreeNode {
142
- const idValue = getByPath(record, config.idField);
143
- const id = idValue != null ? String(idValue) : `__row${idx}`;
144
- return { id, record, children: [], depth, parent };
145
- }
146
-
147
- export function findNodeById(roots: TreeNode[], id: string): TreeNode | null {
148
- for (const root of roots) {
149
- if (root.id === id) return root;
150
- const inChild = findNodeById(root.children, id);
151
- if (inChild) return inChild;
152
- }
153
- return null;
154
- }
155
-
156
- export function* walk(node: TreeNode): Generator<TreeNode> {
157
- yield node;
158
- for (const c of node.children) yield* walk(c);
159
- }
160
-
161
- export function flatten(node: TreeNode): DynamicRecord[] {
162
- const out: DynamicRecord[] = [];
163
- for (const n of walk(node)) out.push(n.record);
164
- return out;
165
- }
166
-
167
- export function descendantIds(node: TreeNode): Set<string> {
168
- const out = new Set<string>();
169
- for (const n of walk(node)) out.add(n.id);
170
- return out;
171
- }
172
-
173
- export function allIds(roots: TreeNode[]): Set<string> {
174
- const out = new Set<string>();
175
- for (const r of roots) for (const n of walk(r)) out.add(n.id);
176
- return out;
177
- }
178
-
179
- export function initialExpansion(roots: TreeNode[], mode: "all" | "roots" | "none"): Set<string> {
180
- if (mode === "none") return new Set();
181
- if (mode === "roots") return new Set(roots.map((r) => r.id));
182
- return allIds(roots);
183
- }
184
-
185
- export function flattenAll(data: DynamicRecord[], childrenField: string): DynamicRecord[] {
186
- const out: DynamicRecord[] = [];
187
- const dfs = (record: DynamicRecord) => {
188
- if (record == null || typeof record !== "object") return;
189
- const { [childrenField]: kids, ...rest } = record;
190
- out.push(rest as DynamicRecord);
191
- if (Array.isArray(kids)) for (const k of kids) dfs(k);
192
- };
193
- for (const r of data) dfs(r);
194
- return out;
195
- }
196
-
197
- export type MoveArgs = {
198
- dragIds: string[];
199
- parentId: string | null;
200
- index: number;
201
- };
202
-
203
- export function applyMove(
204
- data: DynamicRecord[],
205
- config: ResolvedTreeConfig,
206
- args: MoveArgs,
207
- ): DynamicRecord[] {
208
- if (config.childrenField) return applyMoveNested(data, config, args);
209
- if (config.parentField) return applyMoveFlat(data, config, args);
210
- return applyMoveTopLevel(data, config, args);
211
- }
212
-
213
- function recordId(record: DynamicRecord, idField: string, fallbackIdx: number): string {
214
- const v = getByPath(record, idField);
215
- return v != null ? String(v) : `__row${fallbackIdx}`;
216
- }
217
-
218
- function applyMoveTopLevel(
219
- data: DynamicRecord[],
220
- config: ResolvedTreeConfig,
221
- args: MoveArgs,
222
- ): DynamicRecord[] {
223
- const ids = new Set(args.dragIds);
224
- const dragged: DynamicRecord[] = [];
225
- const remaining: DynamicRecord[] = [];
226
- data.forEach((rec, i) => {
227
- if (ids.has(recordId(rec, config.idField, i))) dragged.push(rec);
228
- else remaining.push(rec);
229
- });
230
- const target = args.parentId == null ? remaining : remaining;
231
- const insertAt = Math.max(0, Math.min(args.index, target.length));
232
- return [...target.slice(0, insertAt), ...dragged, ...target.slice(insertAt)];
233
- }
234
-
235
- function applyMoveFlat(
236
- data: DynamicRecord[],
237
- config: ResolvedTreeConfig,
238
- args: MoveArgs,
239
- ): DynamicRecord[] {
240
- const idField = config.idField;
241
- const parentField = config.parentField!;
242
- const orderField = config.orderField;
243
-
244
- const dragSet = new Set(args.dragIds);
245
-
246
- if (args.parentId) {
247
- for (const d of data) {
248
- const id = String(getByPath(d, idField) ?? "");
249
- if (!dragSet.has(id)) continue;
250
- let cur: string | null = args.parentId;
251
- while (cur) {
252
- if (cur === id) return data;
253
- const search = cur;
254
- const parentRec = data.find((r) => String(getByPath(r, idField) ?? "") === search);
255
- if (!parentRec) break;
256
- const next = getByPath(parentRec, parentField);
257
- cur = next == null ? null : String(next);
258
- }
259
- }
260
- }
261
-
262
- const updatedParent: DynamicRecord[] = data.map((rec) => {
263
- const id = String(getByPath(rec, idField) ?? "");
264
- if (!dragSet.has(id)) return rec;
265
- return setByPath(rec, parentField, args.parentId ?? null);
266
- });
267
-
268
- if (!orderField) return updatedParent;
269
-
270
- const siblingsAfter = updatedParent.filter((rec) => {
271
- const p = getByPath(rec, parentField);
272
- const norm = p == null ? null : String(p);
273
- return norm === (args.parentId ?? null);
274
- });
275
-
276
- const dragged: DynamicRecord[] = [];
277
- const others: DynamicRecord[] = [];
278
- siblingsAfter.forEach((rec) => {
279
- const id = String(getByPath(rec, idField) ?? "");
280
- if (dragSet.has(id)) dragged.push(rec);
281
- else others.push(rec);
282
- });
283
-
284
- const insertAt = Math.max(0, Math.min(args.index, others.length));
285
- const newOrder = [...others.slice(0, insertAt), ...dragged, ...others.slice(insertAt)];
286
- const orderById = new Map<string, number>();
287
- newOrder.forEach((rec, i) => {
288
- const id = String(getByPath(rec, idField) ?? "");
289
- orderById.set(id, i);
290
- });
291
-
292
- return updatedParent.map((rec) => {
293
- const id = String(getByPath(rec, idField) ?? "");
294
- if (!orderById.has(id)) return rec;
295
- return setByPath(rec, orderField, orderById.get(id)!);
296
- });
297
- }
298
-
299
- function applyMoveNested(
300
- data: DynamicRecord[],
301
- config: ResolvedTreeConfig,
302
- args: MoveArgs,
303
- ): DynamicRecord[] {
304
- const childrenField = config.childrenField!;
305
- const idField = config.idField;
306
- const dragSet = new Set(args.dragIds);
307
-
308
- const isAncestorOfDrag = (record: DynamicRecord, ancestors: DynamicRecord[]): boolean => {
309
- const id = String(getByPath(record, idField) ?? "");
310
- if (dragSet.has(id)) {
311
- for (const a of ancestors) {
312
- const aid = String(getByPath(a, idField) ?? "");
313
- if (aid === args.parentId) return true;
314
- }
315
- }
316
- const kids = getByPath(record, childrenField);
317
- if (Array.isArray(kids)) {
318
- for (const k of kids) {
319
- if (isAncestorOfDrag(k, [...ancestors, record])) return true;
320
- }
321
- }
322
- return false;
323
- };
324
- for (const r of data) if (isAncestorOfDrag(r, [])) return data;
325
-
326
- const extracted: DynamicRecord[] = [];
327
- const extract = (records: DynamicRecord[]): DynamicRecord[] =>
328
- records
329
- .map((rec) => {
330
- const id = String(getByPath(rec, idField) ?? "");
331
- const kids = getByPath(rec, childrenField);
332
- const newKids = Array.isArray(kids) ? extract(kids) : kids;
333
- const next = Array.isArray(kids) ? setByPath(rec, childrenField, newKids) : rec;
334
- if (dragSet.has(id)) {
335
- extracted.push(next);
336
- return null;
337
- }
338
- return next;
339
- })
340
- .filter((r): r is DynamicRecord => r !== null);
341
-
342
- const pruned = extract(data);
343
-
344
- const insert = (records: DynamicRecord[]): DynamicRecord[] =>
345
- records.map((rec) => {
346
- const id = String(getByPath(rec, idField) ?? "");
347
- if (id === args.parentId) {
348
- const kids = getByPath(rec, childrenField);
349
- const arr = Array.isArray(kids) ? [...kids] : [];
350
- const at = Math.max(0, Math.min(args.index, arr.length));
351
- arr.splice(at, 0, ...extracted);
352
- return setByPath(rec, childrenField, arr);
353
- }
354
- const kids = getByPath(rec, childrenField);
355
- if (Array.isArray(kids)) {
356
- return setByPath(rec, childrenField, insert(kids));
357
- }
358
- return rec;
359
- });
360
-
361
- if (args.parentId == null) {
362
- const at = Math.max(0, Math.min(args.index, pruned.length));
363
- return [...pruned.slice(0, at), ...extracted, ...pruned.slice(at)];
364
- }
365
-
366
- return insert(pruned);
367
- }
368
-
369
- export function pruneTree(
370
- roots: TreeNode[],
371
- predicate: (record: DynamicRecord) => boolean,
372
- ): TreeNode[] {
373
- const visit = (node: TreeNode, parent: TreeNode | null): TreeNode | null => {
374
- const newChildren: TreeNode[] = [];
375
- for (const c of node.children) {
376
- const next = visit(c, node);
377
- if (next) newChildren.push(next);
378
- }
379
- const selfMatches = predicate(node.record);
380
- if (!selfMatches && newChildren.length === 0) return null;
381
- return {
382
- id: node.id,
383
- record: node.record,
384
- depth: node.depth,
385
- parent,
386
- children: newChildren,
387
- };
388
- };
389
-
390
- const out: TreeNode[] = [];
391
- for (const r of roots) {
392
- const next = visit(r, null);
393
- if (next) out.push(next);
394
- }
395
- return out;
396
- }