torch-glare 2.1.1 → 2.1.2
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/BadgeField.tsx +2 -2
- package/apps/lib/components/DataViews/ARCHITECTURE.md +439 -0
- package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +416 -0
- package/apps/lib/components/DataViews/DataViewsHeader.tsx +126 -0
- package/apps/lib/components/DataViews/DataViewsLayout.tsx +300 -0
- package/apps/lib/components/DataViews/FilterPanel.tsx +324 -0
- package/apps/lib/components/DataViews/InboxView.tsx +514 -0
- package/apps/lib/components/DataViews/KanbanView.tsx +242 -0
- package/apps/lib/components/DataViews/PanelControls.tsx +80 -0
- package/apps/lib/components/DataViews/SettingsPanel.tsx +285 -0
- package/apps/lib/components/DataViews/TableView.tsx +232 -0
- package/apps/lib/components/DataViews/TreeView.tsx +363 -0
- package/apps/lib/components/DataViews/badgeAdapter.ts +45 -0
- package/apps/lib/components/DataViews/fieldRenderers.tsx +334 -0
- package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +113 -0
- package/apps/lib/components/DataViews/filters/PresetChips.tsx +45 -0
- package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +154 -0
- package/apps/lib/components/DataViews/index.ts +30 -0
- package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +54 -0
- package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +77 -0
- package/apps/lib/components/DataViews/types.ts +177 -0
- package/apps/lib/components/TreeFolder/TreeFolder.tsx +387 -0
- package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +80 -0
- package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +235 -0
- package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +60 -0
- package/apps/lib/components/TreeFolder/icons.tsx +63 -0
- package/apps/lib/components/TreeFolder/index.ts +17 -0
- package/apps/lib/components/TreeFolder/treeFolderUtils.ts +114 -0
- package/apps/lib/components/TreeFolder/types.ts +68 -0
- package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +261 -0
- package/apps/lib/hooks/useDataViewsState.ts +169 -0
- package/apps/lib/hooks/useIsMobile.ts +21 -0
- package/apps/lib/utils/dataViews/columnUtils.ts +130 -0
- package/apps/lib/utils/dataViews/fieldUtils.ts +198 -0
- package/apps/lib/utils/dataViews/nestedDataUtils.tsx +364 -0
- package/apps/lib/utils/dataViews/pathUtils.ts +132 -0
- package/apps/lib/utils/dataViews/rangeUtils.ts +225 -0
- package/apps/lib/utils/dataViews/treeUtils.ts +403 -0
- package/dist/bin/index.js +3 -3
- package/dist/bin/index.js.map +1 -1
- package/dist/src/commands/add.d.ts.map +1 -1
- package/dist/src/commands/add.js +29 -6
- package/dist/src/commands/add.js.map +1 -1
- package/dist/src/commands/utils.d.ts.map +1 -1
- package/dist/src/commands/utils.js +22 -2
- package/dist/src/commands/utils.js.map +1 -1
- package/dist/src/shared/copyComponentsRecursively.d.ts.map +1 -1
- package/dist/src/shared/copyComponentsRecursively.js +8 -1
- package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts +18 -4
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +110 -40
- package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
- package/docs/components/form-stepper.md +244 -0
- package/docs/components/stepper.md +215 -0
- package/docs/components/timeline.md +248 -0
- package/package.json +6 -6
- package/apps/lib/components/Charts-dev.tsx +0 -365
- package/apps/lib/components/Command-dev.tsx +0 -151
- package/apps/lib/components/IosDatePicker-dev.tsx +0 -341
- /package/docs/components/{labeled-checkbox.md → labeled-check-box.md} +0 -0
- /package/docs/components/{tree-dropdown.md → tree-drop-down.md} +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
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": return 0.1
|
|
86
|
+
case "progress-bar": return 1
|
|
87
|
+
case "currency": {
|
|
88
|
+
const span = extremes.max - extremes.min
|
|
89
|
+
if (span >= 10000) return 100
|
|
90
|
+
if (span >= 1000) return 10
|
|
91
|
+
return 1
|
|
92
|
+
}
|
|
93
|
+
default: {
|
|
94
|
+
const span = extremes.max - extremes.min
|
|
95
|
+
if (span >= 10000) return 10
|
|
96
|
+
if (span >= 100) return 1
|
|
97
|
+
return 0.1
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type DateExtremes = { from: string; to: string }
|
|
103
|
+
|
|
104
|
+
export function computeDateExtremes(
|
|
105
|
+
data: DynamicRecord[],
|
|
106
|
+
path: string,
|
|
107
|
+
): DateExtremes | null {
|
|
108
|
+
let minMs = Number.POSITIVE_INFINITY
|
|
109
|
+
let maxMs = Number.NEGATIVE_INFINITY
|
|
110
|
+
let found = false
|
|
111
|
+
for (const item of data) {
|
|
112
|
+
const v = getByPath(item, path)
|
|
113
|
+
if (v == null) continue
|
|
114
|
+
const ms = v instanceof Date ? v.getTime() : Date.parse(String(v))
|
|
115
|
+
if (!Number.isFinite(ms)) continue
|
|
116
|
+
if (ms < minMs) minMs = ms
|
|
117
|
+
if (ms > maxMs) maxMs = ms
|
|
118
|
+
found = true
|
|
119
|
+
}
|
|
120
|
+
if (!found) return null
|
|
121
|
+
return { from: toIsoDate(new Date(minMs)), to: toIsoDate(new Date(maxMs)) }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const RELATIVE = /^(-?\d+)([dwmy])$/
|
|
125
|
+
|
|
126
|
+
export function resolveRelativeDate(token: string | undefined): string | undefined {
|
|
127
|
+
if (!token) return undefined
|
|
128
|
+
if (token === "today" || token === "now") return toIsoDate(new Date())
|
|
129
|
+
if (token === "start-of-year") {
|
|
130
|
+
const now = new Date()
|
|
131
|
+
return toIsoDate(new Date(now.getFullYear(), 0, 1))
|
|
132
|
+
}
|
|
133
|
+
const m = RELATIVE.exec(token)
|
|
134
|
+
if (m) {
|
|
135
|
+
const n = parseInt(m[1], 10)
|
|
136
|
+
const unit = m[2]
|
|
137
|
+
const d = new Date()
|
|
138
|
+
switch (unit) {
|
|
139
|
+
case "d": d.setDate(d.getDate() + n); break
|
|
140
|
+
case "w": d.setDate(d.getDate() + n * 7); break
|
|
141
|
+
case "m": d.setMonth(d.getMonth() + n); break
|
|
142
|
+
case "y": d.setFullYear(d.getFullYear() + n); break
|
|
143
|
+
}
|
|
144
|
+
return toIsoDate(d)
|
|
145
|
+
}
|
|
146
|
+
const ms = Date.parse(token)
|
|
147
|
+
if (Number.isFinite(ms)) return toIsoDate(new Date(ms))
|
|
148
|
+
return token
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function toIsoDate(d: Date): string {
|
|
152
|
+
const yyyy = d.getFullYear()
|
|
153
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0")
|
|
154
|
+
const dd = String(d.getDate()).padStart(2, "0")
|
|
155
|
+
return `${yyyy}-${mm}-${dd}`
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function presetToFilterValue(preset: FieldPreset): RangeFilter {
|
|
159
|
+
if ("from" in preset || "to" in preset) {
|
|
160
|
+
return {
|
|
161
|
+
kind: "date",
|
|
162
|
+
from: resolveRelativeDate(preset.from),
|
|
163
|
+
to: resolveRelativeDate(preset.to),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
kind: "number",
|
|
168
|
+
min: (preset as { min?: number }).min,
|
|
169
|
+
max: (preset as { max?: number }).max,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function isPresetActive(preset: FieldPreset, current: FilterValue | undefined): boolean {
|
|
174
|
+
if (!current) return false
|
|
175
|
+
if (Array.isArray(current)) return false
|
|
176
|
+
if (current.kind === "number" && !("from" in preset) && !("to" in preset)) {
|
|
177
|
+
return (
|
|
178
|
+
((preset as { min?: number }).min ?? null) === (current.min ?? null) &&
|
|
179
|
+
((preset as { max?: number }).max ?? null) === (current.max ?? null)
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
if (current.kind === "date" && (("from" in preset) || ("to" in preset))) {
|
|
183
|
+
const want = presetToFilterValue(preset) as DateRangeFilter
|
|
184
|
+
return (want.from ?? null) === (current.from ?? null) && (want.to ?? null) === (current.to ?? null)
|
|
185
|
+
}
|
|
186
|
+
return false
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const DEFAULT_PRESETS: Record<string, FieldPreset[]> = {
|
|
190
|
+
currency: [
|
|
191
|
+
{ label: "<$100", max: 100 },
|
|
192
|
+
{ label: "$100–$500", min: 100, max: 500 },
|
|
193
|
+
{ label: "$500–$2k", min: 500, max: 2000 },
|
|
194
|
+
{ label: "$2k+", min: 2000 },
|
|
195
|
+
],
|
|
196
|
+
"progress-bar": [
|
|
197
|
+
{ label: "0%", min: 0, max: 0 },
|
|
198
|
+
{ label: "1–40%", min: 1, max: 40 },
|
|
199
|
+
{ label: "40–70%", min: 40, max: 70 },
|
|
200
|
+
{ label: "70–99%", min: 70, max: 99 },
|
|
201
|
+
{ label: "100%", min: 100, max: 100 },
|
|
202
|
+
],
|
|
203
|
+
"star-rating": [
|
|
204
|
+
{ label: "≥4★", min: 4 },
|
|
205
|
+
{ label: "≥3★", min: 3 },
|
|
206
|
+
{ label: "<3★", max: 3 },
|
|
207
|
+
],
|
|
208
|
+
date: [
|
|
209
|
+
{ label: "Today", from: "today" },
|
|
210
|
+
{ label: "Last 7 days", from: "-7d" },
|
|
211
|
+
{ label: "Last 30 days", from: "-30d" },
|
|
212
|
+
{ label: "This year", from: "start-of-year" },
|
|
213
|
+
],
|
|
214
|
+
"date-format": [
|
|
215
|
+
{ label: "Today", from: "today" },
|
|
216
|
+
{ label: "Last 7 days", from: "-7d" },
|
|
217
|
+
{ label: "Last 30 days", from: "-30d" },
|
|
218
|
+
{ label: "This year", from: "start-of-year" },
|
|
219
|
+
],
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function resolvePresets(field: FieldConfig): FieldPreset[] {
|
|
223
|
+
if (field.presets) return field.presets
|
|
224
|
+
return DEFAULT_PRESETS[field.type ?? ""] ?? []
|
|
225
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
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(
|
|
26
|
+
data: DynamicRecord[],
|
|
27
|
+
config: TreeConfig,
|
|
28
|
+
): ResolvedTreeConfig {
|
|
29
|
+
const sample = data.find((r) => r != null && typeof r === "object") ?? {}
|
|
30
|
+
|
|
31
|
+
const idField = config.idField ?? pickKey(sample, ID_PATTERNS) ?? "id"
|
|
32
|
+
|
|
33
|
+
let childrenField = config.childrenField
|
|
34
|
+
let parentField = config.parentField
|
|
35
|
+
|
|
36
|
+
if (!childrenField && !parentField) {
|
|
37
|
+
childrenField = pickArrayKey(sample, CHILDREN_PATTERNS)
|
|
38
|
+
if (!childrenField) parentField = pickKey(sample, PARENT_PATTERNS)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
childrenField,
|
|
43
|
+
parentField,
|
|
44
|
+
idField,
|
|
45
|
+
orderField: config.orderField,
|
|
46
|
+
nodeLabel: config.nodeLabel,
|
|
47
|
+
defaultExpanded: config.defaultExpanded ?? "roots",
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pickKey(record: DynamicRecord, candidates: string[]): string | undefined {
|
|
52
|
+
for (const key of candidates) {
|
|
53
|
+
if (key in record) return key
|
|
54
|
+
}
|
|
55
|
+
return undefined
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function pickArrayKey(record: DynamicRecord, candidates: string[]): string | undefined {
|
|
59
|
+
for (const key of candidates) {
|
|
60
|
+
if (key in record && Array.isArray(record[key])) return key
|
|
61
|
+
}
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildTree(
|
|
66
|
+
data: DynamicRecord[],
|
|
67
|
+
config: ResolvedTreeConfig,
|
|
68
|
+
): TreeNode[] {
|
|
69
|
+
if (!data || data.length === 0) return []
|
|
70
|
+
if (config.childrenField) return buildFromNested(data, config)
|
|
71
|
+
if (config.parentField) return buildFromFlat(data, config)
|
|
72
|
+
return data.map((record, idx) => makeNode(record, config, idx, null, 0))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildFromNested(
|
|
76
|
+
data: DynamicRecord[],
|
|
77
|
+
config: ResolvedTreeConfig,
|
|
78
|
+
): TreeNode[] {
|
|
79
|
+
const seen = new Set<string>()
|
|
80
|
+
|
|
81
|
+
const visit = (record: DynamicRecord, depth: number, parent: TreeNode | null, idx: number): TreeNode => {
|
|
82
|
+
const node = makeNode(record, config, idx, parent, depth)
|
|
83
|
+
if (seen.has(node.id)) {
|
|
84
|
+
return { ...node, children: [] }
|
|
85
|
+
}
|
|
86
|
+
seen.add(node.id)
|
|
87
|
+
|
|
88
|
+
const rawChildren = getByPath(record, config.childrenField)
|
|
89
|
+
if (Array.isArray(rawChildren)) {
|
|
90
|
+
node.children = rawChildren.map((child, ci) => visit(child, depth + 1, node, ci))
|
|
91
|
+
}
|
|
92
|
+
return node
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return data.map((record, idx) => visit(record, 0, null, idx))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function buildFromFlat(
|
|
99
|
+
data: DynamicRecord[],
|
|
100
|
+
config: ResolvedTreeConfig,
|
|
101
|
+
): TreeNode[] {
|
|
102
|
+
const byId = new Map<string, TreeNode>()
|
|
103
|
+
for (let i = 0; i < data.length; i++) {
|
|
104
|
+
const record = data[i]
|
|
105
|
+
const id = String(getByPath(record, config.idField) ?? `__row${i}`)
|
|
106
|
+
byId.set(id, {
|
|
107
|
+
id,
|
|
108
|
+
record,
|
|
109
|
+
children: [],
|
|
110
|
+
depth: 0,
|
|
111
|
+
parent: null,
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const roots: TreeNode[] = []
|
|
116
|
+
for (const node of byId.values()) {
|
|
117
|
+
const parentId = getByPath(node.record, config.parentField)
|
|
118
|
+
if (parentId == null) {
|
|
119
|
+
roots.push(node)
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
const parent = byId.get(String(parentId))
|
|
123
|
+
if (!parent || parent === node) {
|
|
124
|
+
roots.push(node)
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
node.parent = parent
|
|
128
|
+
parent.children.push(node)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const setDepth = (n: TreeNode, d: number, guard = new Set<TreeNode>()) => {
|
|
132
|
+
if (guard.has(n)) return
|
|
133
|
+
guard.add(n)
|
|
134
|
+
n.depth = d
|
|
135
|
+
for (const c of n.children) setDepth(c, d + 1, guard)
|
|
136
|
+
}
|
|
137
|
+
for (const r of roots) setDepth(r, 0)
|
|
138
|
+
|
|
139
|
+
return roots
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function makeNode(
|
|
143
|
+
record: DynamicRecord,
|
|
144
|
+
config: ResolvedTreeConfig,
|
|
145
|
+
idx: number,
|
|
146
|
+
parent: TreeNode | null,
|
|
147
|
+
depth: number,
|
|
148
|
+
): TreeNode {
|
|
149
|
+
const idValue = getByPath(record, config.idField)
|
|
150
|
+
const id = idValue != null ? String(idValue) : `__row${idx}`
|
|
151
|
+
return { id, record, children: [], depth, parent }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function findNodeById(roots: TreeNode[], id: string): TreeNode | null {
|
|
155
|
+
for (const root of roots) {
|
|
156
|
+
if (root.id === id) return root
|
|
157
|
+
const inChild = findNodeById(root.children, id)
|
|
158
|
+
if (inChild) return inChild
|
|
159
|
+
}
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function* walk(node: TreeNode): Generator<TreeNode> {
|
|
164
|
+
yield node
|
|
165
|
+
for (const c of node.children) yield* walk(c)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function flatten(node: TreeNode): DynamicRecord[] {
|
|
169
|
+
const out: DynamicRecord[] = []
|
|
170
|
+
for (const n of walk(node)) out.push(n.record)
|
|
171
|
+
return out
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function descendantIds(node: TreeNode): Set<string> {
|
|
175
|
+
const out = new Set<string>()
|
|
176
|
+
for (const n of walk(node)) out.add(n.id)
|
|
177
|
+
return out
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function allIds(roots: TreeNode[]): Set<string> {
|
|
181
|
+
const out = new Set<string>()
|
|
182
|
+
for (const r of roots) for (const n of walk(r)) out.add(n.id)
|
|
183
|
+
return out
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function initialExpansion(roots: TreeNode[], mode: "all" | "roots" | "none"): Set<string> {
|
|
187
|
+
if (mode === "none") return new Set()
|
|
188
|
+
if (mode === "roots") return new Set(roots.map((r) => r.id))
|
|
189
|
+
return allIds(roots)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function flattenAll(data: DynamicRecord[], childrenField: string): DynamicRecord[] {
|
|
193
|
+
const out: DynamicRecord[] = []
|
|
194
|
+
const dfs = (record: DynamicRecord) => {
|
|
195
|
+
if (record == null || typeof record !== "object") return
|
|
196
|
+
const { [childrenField]: kids, ...rest } = record
|
|
197
|
+
out.push(rest as DynamicRecord)
|
|
198
|
+
if (Array.isArray(kids)) for (const k of kids) dfs(k)
|
|
199
|
+
}
|
|
200
|
+
for (const r of data) dfs(r)
|
|
201
|
+
return out
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export type MoveArgs = {
|
|
205
|
+
dragIds: string[]
|
|
206
|
+
parentId: string | null
|
|
207
|
+
index: number
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function applyMove(
|
|
211
|
+
data: DynamicRecord[],
|
|
212
|
+
config: ResolvedTreeConfig,
|
|
213
|
+
args: MoveArgs,
|
|
214
|
+
): DynamicRecord[] {
|
|
215
|
+
if (config.childrenField) return applyMoveNested(data, config, args)
|
|
216
|
+
if (config.parentField) return applyMoveFlat(data, config, args)
|
|
217
|
+
return applyMoveTopLevel(data, config, args)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function recordId(record: DynamicRecord, idField: string, fallbackIdx: number): string {
|
|
221
|
+
const v = getByPath(record, idField)
|
|
222
|
+
return v != null ? String(v) : `__row${fallbackIdx}`
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function applyMoveTopLevel(
|
|
226
|
+
data: DynamicRecord[],
|
|
227
|
+
config: ResolvedTreeConfig,
|
|
228
|
+
args: MoveArgs,
|
|
229
|
+
): DynamicRecord[] {
|
|
230
|
+
const ids = new Set(args.dragIds)
|
|
231
|
+
const dragged: DynamicRecord[] = []
|
|
232
|
+
const remaining: DynamicRecord[] = []
|
|
233
|
+
data.forEach((rec, i) => {
|
|
234
|
+
if (ids.has(recordId(rec, config.idField, i))) dragged.push(rec)
|
|
235
|
+
else remaining.push(rec)
|
|
236
|
+
})
|
|
237
|
+
const target = args.parentId == null ? remaining : remaining
|
|
238
|
+
const insertAt = Math.max(0, Math.min(args.index, target.length))
|
|
239
|
+
return [...target.slice(0, insertAt), ...dragged, ...target.slice(insertAt)]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function applyMoveFlat(
|
|
243
|
+
data: DynamicRecord[],
|
|
244
|
+
config: ResolvedTreeConfig,
|
|
245
|
+
args: MoveArgs,
|
|
246
|
+
): DynamicRecord[] {
|
|
247
|
+
const idField = config.idField
|
|
248
|
+
const parentField = config.parentField!
|
|
249
|
+
const orderField = config.orderField
|
|
250
|
+
|
|
251
|
+
const dragSet = new Set(args.dragIds)
|
|
252
|
+
|
|
253
|
+
if (args.parentId) {
|
|
254
|
+
for (const d of data) {
|
|
255
|
+
const id = String(getByPath(d, idField) ?? "")
|
|
256
|
+
if (!dragSet.has(id)) continue
|
|
257
|
+
let cur: string | null = args.parentId
|
|
258
|
+
while (cur) {
|
|
259
|
+
if (cur === id) return data
|
|
260
|
+
const search = cur
|
|
261
|
+
const parentRec = data.find((r) => String(getByPath(r, idField) ?? "") === search)
|
|
262
|
+
if (!parentRec) break
|
|
263
|
+
const next = getByPath(parentRec, parentField)
|
|
264
|
+
cur = next == null ? null : String(next)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const updatedParent: DynamicRecord[] = data.map((rec) => {
|
|
270
|
+
const id = String(getByPath(rec, idField) ?? "")
|
|
271
|
+
if (!dragSet.has(id)) return rec
|
|
272
|
+
return setByPath(rec, parentField, args.parentId ?? null)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
if (!orderField) return updatedParent
|
|
276
|
+
|
|
277
|
+
const siblingsAfter = updatedParent.filter((rec) => {
|
|
278
|
+
const p = getByPath(rec, parentField)
|
|
279
|
+
const norm = p == null ? null : String(p)
|
|
280
|
+
return norm === (args.parentId ?? null)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
const dragged: DynamicRecord[] = []
|
|
284
|
+
const others: DynamicRecord[] = []
|
|
285
|
+
siblingsAfter.forEach((rec) => {
|
|
286
|
+
const id = String(getByPath(rec, idField) ?? "")
|
|
287
|
+
if (dragSet.has(id)) dragged.push(rec)
|
|
288
|
+
else others.push(rec)
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const insertAt = Math.max(0, Math.min(args.index, others.length))
|
|
292
|
+
const newOrder = [...others.slice(0, insertAt), ...dragged, ...others.slice(insertAt)]
|
|
293
|
+
const orderById = new Map<string, number>()
|
|
294
|
+
newOrder.forEach((rec, i) => {
|
|
295
|
+
const id = String(getByPath(rec, idField) ?? "")
|
|
296
|
+
orderById.set(id, i)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
return updatedParent.map((rec) => {
|
|
300
|
+
const id = String(getByPath(rec, idField) ?? "")
|
|
301
|
+
if (!orderById.has(id)) return rec
|
|
302
|
+
return setByPath(rec, orderField, orderById.get(id)!)
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function applyMoveNested(
|
|
307
|
+
data: DynamicRecord[],
|
|
308
|
+
config: ResolvedTreeConfig,
|
|
309
|
+
args: MoveArgs,
|
|
310
|
+
): DynamicRecord[] {
|
|
311
|
+
const childrenField = config.childrenField!
|
|
312
|
+
const idField = config.idField
|
|
313
|
+
const dragSet = new Set(args.dragIds)
|
|
314
|
+
|
|
315
|
+
const isAncestorOfDrag = (record: DynamicRecord, ancestors: DynamicRecord[]): boolean => {
|
|
316
|
+
const id = String(getByPath(record, idField) ?? "")
|
|
317
|
+
if (dragSet.has(id)) {
|
|
318
|
+
for (const a of ancestors) {
|
|
319
|
+
const aid = String(getByPath(a, idField) ?? "")
|
|
320
|
+
if (aid === args.parentId) return true
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const kids = getByPath(record, childrenField)
|
|
324
|
+
if (Array.isArray(kids)) {
|
|
325
|
+
for (const k of kids) {
|
|
326
|
+
if (isAncestorOfDrag(k, [...ancestors, record])) return true
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return false
|
|
330
|
+
}
|
|
331
|
+
for (const r of data) if (isAncestorOfDrag(r, [])) return data
|
|
332
|
+
|
|
333
|
+
const extracted: DynamicRecord[] = []
|
|
334
|
+
const extract = (records: DynamicRecord[]): DynamicRecord[] =>
|
|
335
|
+
records
|
|
336
|
+
.map((rec) => {
|
|
337
|
+
const id = String(getByPath(rec, idField) ?? "")
|
|
338
|
+
const kids = getByPath(rec, childrenField)
|
|
339
|
+
const newKids = Array.isArray(kids) ? extract(kids) : kids
|
|
340
|
+
const next = Array.isArray(kids) ? setByPath(rec, childrenField, newKids) : rec
|
|
341
|
+
if (dragSet.has(id)) {
|
|
342
|
+
extracted.push(next)
|
|
343
|
+
return null
|
|
344
|
+
}
|
|
345
|
+
return next
|
|
346
|
+
})
|
|
347
|
+
.filter((r): r is DynamicRecord => r !== null)
|
|
348
|
+
|
|
349
|
+
let pruned = extract(data)
|
|
350
|
+
|
|
351
|
+
const insert = (records: DynamicRecord[]): DynamicRecord[] =>
|
|
352
|
+
records.map((rec) => {
|
|
353
|
+
const id = String(getByPath(rec, idField) ?? "")
|
|
354
|
+
if (id === args.parentId) {
|
|
355
|
+
const kids = getByPath(rec, childrenField)
|
|
356
|
+
const arr = Array.isArray(kids) ? [...kids] : []
|
|
357
|
+
const at = Math.max(0, Math.min(args.index, arr.length))
|
|
358
|
+
arr.splice(at, 0, ...extracted)
|
|
359
|
+
return setByPath(rec, childrenField, arr)
|
|
360
|
+
}
|
|
361
|
+
const kids = getByPath(rec, childrenField)
|
|
362
|
+
if (Array.isArray(kids)) {
|
|
363
|
+
return setByPath(rec, childrenField, insert(kids))
|
|
364
|
+
}
|
|
365
|
+
return rec
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
if (args.parentId == null) {
|
|
369
|
+
const at = Math.max(0, Math.min(args.index, pruned.length))
|
|
370
|
+
return [...pruned.slice(0, at), ...extracted, ...pruned.slice(at)]
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return insert(pruned)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function pruneTree(
|
|
377
|
+
roots: TreeNode[],
|
|
378
|
+
predicate: (record: DynamicRecord) => boolean,
|
|
379
|
+
): TreeNode[] {
|
|
380
|
+
const visit = (node: TreeNode, parent: TreeNode | null): TreeNode | null => {
|
|
381
|
+
const newChildren: TreeNode[] = []
|
|
382
|
+
for (const c of node.children) {
|
|
383
|
+
const next = visit(c, node)
|
|
384
|
+
if (next) newChildren.push(next)
|
|
385
|
+
}
|
|
386
|
+
const selfMatches = predicate(node.record)
|
|
387
|
+
if (!selfMatches && newChildren.length === 0) return null
|
|
388
|
+
return {
|
|
389
|
+
id: node.id,
|
|
390
|
+
record: node.record,
|
|
391
|
+
depth: node.depth,
|
|
392
|
+
parent,
|
|
393
|
+
children: newChildren,
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const out: TreeNode[] = []
|
|
398
|
+
for (const r of roots) {
|
|
399
|
+
const next = visit(r, null)
|
|
400
|
+
if (next) out.push(next)
|
|
401
|
+
}
|
|
402
|
+
return out
|
|
403
|
+
}
|
package/dist/bin/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const program = new Command();
|
|
|
12
12
|
program
|
|
13
13
|
.name("torch-glare")
|
|
14
14
|
.description("Torch Glare for managing React components")
|
|
15
|
-
.version("
|
|
15
|
+
.version("2.2.0");
|
|
16
16
|
program
|
|
17
17
|
.command("init")
|
|
18
18
|
.description("Initialize torch.json configuration file")
|
|
@@ -20,7 +20,7 @@ program
|
|
|
20
20
|
program
|
|
21
21
|
.command("add [component]")
|
|
22
22
|
.description("Add a component interactively or install a specified one")
|
|
23
|
-
.action((component) => add(component
|
|
23
|
+
.action((component) => add(component));
|
|
24
24
|
program
|
|
25
25
|
.command("hook [hook]")
|
|
26
26
|
.description("Add a hook interactively or install a specified one")
|
|
@@ -32,7 +32,7 @@ program
|
|
|
32
32
|
program
|
|
33
33
|
.command("util [util]")
|
|
34
34
|
.description("Add a utils interactively or install a specified one")
|
|
35
|
-
.action((util) => addUtil(util
|
|
35
|
+
.action((util) => addUtil(util));
|
|
36
36
|
program
|
|
37
37
|
.command("provider [provider]")
|
|
38
38
|
.description("Add a provider interactively or install a specified one")
|
package/dist/bin/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../cli/bin/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,aAAa,CAAC;KACnB,WAAW,CAAC,2CAA2C,CAAC;KACxD,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,0CAA0C,CAAC;KACvD,MAAM,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;AAE9B,OAAO;KACJ,OAAO,CAAC,iBAAiB,CAAC;KAC1B,WAAW,CAAC,0DAA0D,CAAC;KACvE,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../cli/bin/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,wBAAwB,CAAC;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC1D,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAElD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,aAAa,CAAC;KACnB,WAAW,CAAC,2CAA2C,CAAC;KACxD,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,WAAW,CAAC,0CAA0C,CAAC;KACvD,MAAM,CAAC,GAAG,EAAE,CAAC,UAAU,EAAE,CAAC,CAAC;AAE9B,OAAO;KACJ,OAAO,CAAC,iBAAiB,CAAC;KAC1B,WAAW,CAAC,0DAA0D,CAAC;KACvE,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;AAEzC,OAAO;KACJ,OAAO,CAAC,aAAa,CAAC;KACtB,WAAW,CAAC,qDAAqD,CAAC;KAClE,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,IAAI,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC;AAEhD,OAAO;KACJ,OAAO,CAAC,iBAAiB,CAAC;KAC1B,WAAW,CAAC,uDAAuD,CAAC;KACpE,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,SAAS,CAAC,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,CAAC,CAAC;AAE5D,OAAO;KACJ,OAAO,CAAC,aAAa,CAAC;KACtB,WAAW,CAAC,sDAAsD,CAAC;KACnE,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AAEnC,OAAO;KACJ,OAAO,CAAC,qBAAqB,CAAC;KAC9B,WAAW,CAAC,yDAAyD,CAAC;KACtE,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,WAAW,CAAC,QAAQ,IAAI,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC;AAEhE,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,WAAW,CAAC,6BAA6B,CAAC;KAC1C,MAAM,CAAC,GAAG,EAAE,CAAC,yBAAyB,EAAE,CAAC,CAAC;AAE7C,OAAO;KACJ,OAAO,CAAC,KAAK,CAAC;KACd,WAAW,CAAC,kDAAkD,CAAC;KAC/D,MAAM,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC;AAE5B,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../../cli/src/commands/add.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../../cli/src/commands/add.ts"],"names":[],"mappings":"AAuBA;;;;GAIG;AACH,wBAAsB,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE,OAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAiCrF"}
|