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.
- package/apps/lib/components/DataViews/{badgeAdapter.ts → badge.ts} +2 -2
- package/apps/lib/components/DataViews/cell.tsx +324 -0
- package/apps/lib/components/DataViews/context.ts +144 -0
- package/apps/lib/components/DataViews/data-views.tsx +383 -0
- package/apps/lib/components/DataViews/filters/children.tsx +98 -0
- package/apps/lib/components/DataViews/filters/custom.tsx +34 -0
- package/apps/lib/components/DataViews/filters/filters.tsx +163 -0
- package/apps/lib/components/DataViews/filters/index.ts +4 -0
- package/apps/lib/components/DataViews/filters/labelled.tsx +20 -0
- package/apps/lib/components/DataViews/filters/presets.tsx +65 -0
- package/apps/lib/components/DataViews/filters/summary.tsx +65 -0
- package/apps/lib/components/DataViews/filters/sync.tsx +35 -0
- package/apps/lib/components/DataViews/filters/values.ts +173 -0
- package/apps/lib/components/DataViews/header.tsx +217 -0
- package/apps/lib/components/DataViews/hooks/index.ts +5 -0
- package/apps/lib/components/DataViews/hooks/useActiveRow.ts +22 -0
- package/apps/lib/components/DataViews/hooks/useControllable.ts +52 -0
- package/apps/lib/components/DataViews/index.ts +74 -26
- package/apps/lib/components/DataViews/panel/columns.tsx +153 -0
- package/apps/lib/components/DataViews/panel/controls.tsx +106 -0
- package/apps/lib/components/DataViews/panel/index.ts +3 -0
- package/apps/lib/components/DataViews/panel/panel.tsx +164 -0
- package/apps/lib/components/DataViews/panel/saved-views.tsx +67 -0
- package/apps/lib/components/DataViews/panel/section.tsx +79 -0
- package/apps/lib/components/DataViews/panel/sort.tsx +42 -0
- package/apps/lib/components/DataViews/panel/tab.tsx +31 -0
- package/apps/lib/components/DataViews/slots.ts +63 -0
- package/apps/lib/components/DataViews/states.tsx +38 -0
- package/apps/lib/components/DataViews/types.ts +485 -178
- package/apps/lib/components/DataViews/views/board-view.tsx +379 -0
- package/apps/lib/components/DataViews/views/card-rows.tsx +36 -0
- package/apps/lib/components/DataViews/views/inbox-view.tsx +257 -0
- package/apps/lib/components/DataViews/views/pane-views.tsx +192 -0
- package/apps/lib/components/DataViews/views/table-view.tsx +426 -0
- package/apps/lib/components/DataViews/views/tree-view.tsx +365 -0
- package/apps/lib/components/FormBuilder/context.ts +20 -6
- package/apps/lib/components/FormBuilder/field-kind.ts +28 -0
- package/apps/lib/components/FormBuilder/fields/DateField.tsx +3 -3
- package/apps/lib/components/FormBuilder/fields/FieldShell.tsx +7 -6
- package/apps/lib/components/FormBuilder/fields/PhoneField.tsx +30 -4
- package/apps/lib/components/FormBuilder/fields/SelectField.tsx +7 -7
- package/apps/lib/components/FormBuilder/fields/TableField.tsx +80 -52
- package/apps/lib/components/FormBuilder/fields/TextField.tsx +9 -9
- package/apps/lib/components/FormBuilder/form-builder.tsx +66 -6
- package/apps/lib/components/FormBuilder/index.ts +3 -1
- package/apps/lib/components/FormBuilder/types.ts +40 -0
- package/apps/lib/components/Input.tsx +3 -0
- package/apps/lib/components/SearchableTable.tsx +5 -4
- package/apps/lib/components/SectionBlock.tsx +58 -11
- package/apps/lib/components/Select.tsx +3 -1
- package/apps/lib/components/TabSwitch.tsx +16 -4
- package/apps/lib/components/Table.tsx +265 -67
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +6 -3
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +16 -14
- package/apps/lib/components/TreeFolder/index.ts +1 -1
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +70 -207
- package/apps/lib/hooks/useDragDrop.tsx +365 -0
- package/apps/lib/hooks/useInfiniteScroll.ts +108 -0
- package/apps/lib/registry.json +159 -4
- package/apps/lib/tsconfig.tsbuildinfo +1 -1
- package/apps/lib/utils/dataViews/path.ts +67 -0
- package/apps/lib/utils/dataViews/query.ts +73 -0
- package/apps/lib/utils/dataViews/types.ts +187 -0
- package/docs/components/breadcrumb.md +1 -1
- package/docs/components/button-group.md +1 -1
- package/docs/components/button.md +1 -1
- package/docs/components/card.md +1 -1
- package/docs/components/checkbox.md +1 -1
- package/docs/components/data-views/backend-response.md +324 -0
- package/docs/components/data-views/examples/a11y-rtl.md +250 -0
- package/docs/components/data-views/examples/api-orders-route.md +130 -0
- package/docs/components/data-views/examples/fields.md +362 -0
- package/docs/components/data-views/examples/filters.md +308 -0
- package/docs/components/data-views/examples/inbox-routing.md +218 -0
- package/docs/components/data-views/examples/index.md +29 -0
- package/docs/components/data-views/examples/overview.md +244 -0
- package/docs/components/data-views/examples/panel.md +212 -0
- package/docs/components/data-views/examples/scale.md +231 -0
- package/docs/components/data-views/examples/server-side.md +210 -0
- package/docs/components/data-views/examples/state.md +250 -0
- package/docs/components/data-views/examples/tree-custom.md +388 -0
- package/docs/components/data-views/examples/view-registry.md +313 -0
- package/docs/components/data-views/examples/views.md +534 -0
- package/docs/components/data-views/guide.md +405 -0
- package/docs/components/data-views/index.md +1504 -0
- package/docs/components/data-views/migration.md +79 -0
- package/docs/components/date-picker.md +0 -1
- package/docs/components/form-builder.md +19 -8
- package/docs/components/form-renderer.md +2 -1
- package/docs/components/form.md +1 -1
- package/docs/components/input-field.md +1 -1
- package/docs/components/input-otp.md +1 -1
- package/docs/components/input.md +1 -1
- package/docs/components/labeled-check-box.md +1 -1
- package/docs/components/labeled-radio.md +1 -1
- package/docs/components/radio-card.md +1 -1
- package/docs/components/radio.md +1 -1
- package/docs/components/search-field.md +1 -1
- package/docs/components/section-block.md +79 -3
- package/docs/components/select.md +1 -1
- package/docs/components/simple-select.md +1 -1
- package/docs/components/switch.md +1 -1
- package/docs/components/tab-switch.md +1 -1
- package/docs/components/table.md +45 -8
- package/docs/components/text-editor.md +1 -1
- package/docs/components/textarea.md +1 -1
- package/docs/components/toggle-button.md +1 -1
- package/docs/components/toggle.md +1 -1
- package/docs/components/tree-folder.md +110 -0
- package/docs/how-to/forms-with-form-builder.md +6 -4
- package/docs/reference/components.md +16 -6
- package/docs/tutorials/component-composition.md +11 -13
- package/package.json +3 -2
- package/apps/lib/components/DataViews/DataViewRadio.tsx +0 -49
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +0 -393
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +0 -207
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +0 -332
- package/apps/lib/components/DataViews/FilterPanel.tsx +0 -493
- package/apps/lib/components/DataViews/HeaderSearch.tsx +0 -93
- package/apps/lib/components/DataViews/InboxView.tsx +0 -463
- package/apps/lib/components/DataViews/InboxViewCard.tsx +0 -127
- package/apps/lib/components/DataViews/KanbanView.tsx +0 -336
- package/apps/lib/components/DataViews/PanelControls.tsx +0 -39
- package/apps/lib/components/DataViews/SettingsPanel.tsx +0 -279
- package/apps/lib/components/DataViews/TableView.tsx +0 -212
- package/apps/lib/components/DataViews/TreeView.tsx +0 -364
- package/apps/lib/components/DataViews/fieldRenderers.tsx +0 -299
- package/apps/lib/components/DataViews/filters/DatePickerRangeFilter.tsx +0 -87
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +0 -120
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +0 -45
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +0 -165
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +0 -50
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +0 -74
- package/apps/lib/hooks/useDataViewsState.ts +0 -175
- package/apps/lib/utils/dataViews/columnUtils.ts +0 -132
- package/apps/lib/utils/dataViews/fieldUtils.ts +0 -197
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +0 -371
- package/apps/lib/utils/dataViews/pathUtils.ts +0 -139
- package/apps/lib/utils/dataViews/rangeUtils.ts +0 -234
- package/apps/lib/utils/dataViews/treeUtils.ts +0 -396
- package/docs/components/data-views-config-panel.md +0 -208
- package/docs/components/data-views-layout.md +0 -291
- package/docs/components/inbox-view.md +0 -170
- package/docs/components/kanban-view.md +0 -135
- package/docs/components/table-view.md +0 -141
- package/docs/components/tree-view.md +0 -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
|
-
}
|