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.
Files changed (62) hide show
  1. package/apps/lib/components/BadgeField.tsx +2 -2
  2. package/apps/lib/components/DataViews/ARCHITECTURE.md +439 -0
  3. package/apps/lib/components/DataViews/DataViewsConfigPanel.tsx +416 -0
  4. package/apps/lib/components/DataViews/DataViewsHeader.tsx +126 -0
  5. package/apps/lib/components/DataViews/DataViewsLayout.tsx +300 -0
  6. package/apps/lib/components/DataViews/FilterPanel.tsx +324 -0
  7. package/apps/lib/components/DataViews/InboxView.tsx +514 -0
  8. package/apps/lib/components/DataViews/KanbanView.tsx +242 -0
  9. package/apps/lib/components/DataViews/PanelControls.tsx +80 -0
  10. package/apps/lib/components/DataViews/SettingsPanel.tsx +285 -0
  11. package/apps/lib/components/DataViews/TableView.tsx +232 -0
  12. package/apps/lib/components/DataViews/TreeView.tsx +363 -0
  13. package/apps/lib/components/DataViews/badgeAdapter.ts +45 -0
  14. package/apps/lib/components/DataViews/fieldRenderers.tsx +334 -0
  15. package/apps/lib/components/DataViews/filters/DateRangePopover.tsx +113 -0
  16. package/apps/lib/components/DataViews/filters/PresetChips.tsx +45 -0
  17. package/apps/lib/components/DataViews/filters/RangeSliderWithInputs.tsx +154 -0
  18. package/apps/lib/components/DataViews/index.ts +30 -0
  19. package/apps/lib/components/DataViews/tree/TreeDrawer.tsx +54 -0
  20. package/apps/lib/components/DataViews/tree/TreeSidebar.tsx +77 -0
  21. package/apps/lib/components/DataViews/types.ts +177 -0
  22. package/apps/lib/components/TreeFolder/TreeFolder.tsx +387 -0
  23. package/apps/lib/components/TreeFolder/TreeFolderBreadcrumb.tsx +80 -0
  24. package/apps/lib/components/TreeFolder/TreeFolderRow.tsx +235 -0
  25. package/apps/lib/components/TreeFolder/TreeFolderStyles.tsx +60 -0
  26. package/apps/lib/components/TreeFolder/icons.tsx +63 -0
  27. package/apps/lib/components/TreeFolder/index.ts +17 -0
  28. package/apps/lib/components/TreeFolder/treeFolderUtils.ts +114 -0
  29. package/apps/lib/components/TreeFolder/types.ts +68 -0
  30. package/apps/lib/components/TreeFolder/useTreeFolderDnD.ts +261 -0
  31. package/apps/lib/hooks/useDataViewsState.ts +169 -0
  32. package/apps/lib/hooks/useIsMobile.ts +21 -0
  33. package/apps/lib/utils/dataViews/columnUtils.ts +130 -0
  34. package/apps/lib/utils/dataViews/fieldUtils.ts +198 -0
  35. package/apps/lib/utils/dataViews/nestedDataUtils.tsx +364 -0
  36. package/apps/lib/utils/dataViews/pathUtils.ts +132 -0
  37. package/apps/lib/utils/dataViews/rangeUtils.ts +225 -0
  38. package/apps/lib/utils/dataViews/treeUtils.ts +403 -0
  39. package/dist/bin/index.js +3 -3
  40. package/dist/bin/index.js.map +1 -1
  41. package/dist/src/commands/add.d.ts.map +1 -1
  42. package/dist/src/commands/add.js +29 -6
  43. package/dist/src/commands/add.js.map +1 -1
  44. package/dist/src/commands/utils.d.ts.map +1 -1
  45. package/dist/src/commands/utils.js +22 -2
  46. package/dist/src/commands/utils.js.map +1 -1
  47. package/dist/src/shared/copyComponentsRecursively.d.ts.map +1 -1
  48. package/dist/src/shared/copyComponentsRecursively.js +8 -1
  49. package/dist/src/shared/copyComponentsRecursively.js.map +1 -1
  50. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts +18 -4
  51. package/dist/src/shared/getDependenciesAndInstallNestedComponents.d.ts.map +1 -1
  52. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js +110 -40
  53. package/dist/src/shared/getDependenciesAndInstallNestedComponents.js.map +1 -1
  54. package/docs/components/form-stepper.md +244 -0
  55. package/docs/components/stepper.md +215 -0
  56. package/docs/components/timeline.md +248 -0
  57. package/package.json +6 -6
  58. package/apps/lib/components/Charts-dev.tsx +0 -365
  59. package/apps/lib/components/Command-dev.tsx +0 -151
  60. package/apps/lib/components/IosDatePicker-dev.tsx +0 -341
  61. /package/docs/components/{labeled-checkbox.md → labeled-check-box.md} +0 -0
  62. /package/docs/components/{tree-dropdown.md → tree-drop-down.md} +0 -0
@@ -0,0 +1,198 @@
1
+ import type {
2
+ DynamicRecord,
3
+ FieldConfig,
4
+ FieldType,
5
+ DynamicColumnConfig,
6
+ InboxConfig,
7
+ } from "../../components/DataViews/types"
8
+ import { findFirstDefined, formatPathLabel, getByPath } from "./pathUtils"
9
+ import { isPlainObject, isCurrencyField, isRatingField } from "./nestedDataUtils"
10
+
11
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}/
12
+
13
+ const KEY_HINTS: Array<[RegExp, FieldType]> = [
14
+ [/(^|[._])status$/i, "enum-badge"],
15
+ [/(^|[._])priority$/i, "enum-badge"],
16
+ [/(^|[._])avatar(url)?$/i, "avatar"],
17
+ [/(^|[._])email$/i, "link"],
18
+ [/(^|[._])phone$/i, "link"],
19
+ [/(^|[._])url$|website$|homepage$/i, "link"],
20
+ [/(^|[._])(image|thumbnail|photo|picture)$/i, "image"],
21
+ [/(^|[._])tags?$|labels?$/i, "badge-array"],
22
+ [/(date|time)$/i, "date-format"],
23
+ ]
24
+
25
+ export function inferFieldType(path: string, value: any): FieldType {
26
+ for (const [re, type] of KEY_HINTS) {
27
+ if (re.test(path)) return type
28
+ }
29
+
30
+ if (isCurrencyField(path)) return "currency"
31
+ if (isRatingField(path) && typeof value === "number" && value <= 5) return "star-rating"
32
+
33
+ if (value === null || value === undefined) return "text"
34
+ if (typeof value === "boolean") return "boolean"
35
+ if (typeof value === "number") return "number"
36
+ if (Array.isArray(value)) return "badge-array"
37
+ if (typeof value === "string") {
38
+ if (ISO_DATE.test(value) && !isNaN(Date.parse(value))) return "date-format"
39
+ return "text"
40
+ }
41
+ return "text"
42
+ }
43
+
44
+ export function detectFields(data: DynamicRecord[]): FieldConfig[] {
45
+ if (!data || data.length === 0) return []
46
+
47
+ const allKeys = new Set<string>()
48
+ for (const item of data) {
49
+ if (item && typeof item === "object") {
50
+ for (const k of Object.keys(item)) allKeys.add(k)
51
+ }
52
+ }
53
+
54
+ const fields: FieldConfig[] = []
55
+ let order = 0
56
+
57
+ for (const key of allKeys) {
58
+ if (key.startsWith("_")) continue
59
+
60
+ const sample = findFirstDefined(data, key)
61
+ if (isPlainObject(sample)) continue
62
+ if (Array.isArray(sample) && sample.length > 0 && isPlainObject(sample[0])) continue
63
+
64
+ fields.push({
65
+ path: key,
66
+ label: formatPathLabel(key),
67
+ type: inferFieldType(key, sample),
68
+ visible: true,
69
+ order: order++,
70
+ })
71
+ }
72
+
73
+ return fields
74
+ .sort((a, b) => {
75
+ if (a.path === "id") return -1
76
+ if (b.path === "id") return 1
77
+ return (a.label || "").localeCompare(b.label || "")
78
+ })
79
+ .map((f, i) => ({ ...f, order: i }))
80
+ }
81
+
82
+ export function mergeFields(
83
+ detected: FieldConfig[],
84
+ custom?: FieldConfig[],
85
+ ): FieldConfig[] {
86
+ if (!custom || custom.length === 0) return detected
87
+
88
+ const out = [...detected]
89
+ const byPath = new Map(out.map((f, i) => [f.path, i]))
90
+
91
+ for (const c of custom) {
92
+ if (!c.path) continue
93
+ const existing = byPath.get(c.path)
94
+ if (existing != null) {
95
+ out[existing] = { ...out[existing], ...c }
96
+ } else {
97
+ out.push({
98
+ ...c,
99
+ path: c.path,
100
+ label: c.label ?? formatPathLabel(c.path),
101
+ visible: c.visible ?? true,
102
+ order: c.order ?? out.length,
103
+ })
104
+ byPath.set(c.path, out.length - 1)
105
+ }
106
+ }
107
+
108
+ return out
109
+ .map((f, i) => ({ ...f, order: f.order ?? i }))
110
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
111
+ }
112
+
113
+ export function visibleFields(fields: FieldConfig[]): FieldConfig[] {
114
+ return fields.filter((f) => f.type !== "hidden" && f.visible !== false)
115
+ }
116
+
117
+ export function fieldToColumn(field: FieldConfig, idx = 0): DynamicColumnConfig & {
118
+ __field: FieldConfig
119
+ } {
120
+ const legacyType = mapFieldTypeToColumnType(field.type)
121
+ return {
122
+ id: field.path,
123
+ label: field.label ?? formatPathLabel(field.path),
124
+ visible: field.visible !== false && field.type !== "hidden",
125
+ order: field.order ?? idx,
126
+ type: legacyType,
127
+ render: field.render,
128
+ __field: field,
129
+ }
130
+ }
131
+
132
+ function mapFieldTypeToColumnType(t?: FieldType): DynamicColumnConfig["type"] {
133
+ switch (t) {
134
+ case "number":
135
+ case "currency":
136
+ case "number-format":
137
+ case "progress-bar":
138
+ case "star-rating":
139
+ return "number"
140
+ case "date":
141
+ case "date-format":
142
+ return "date"
143
+ case "enum-badge":
144
+ case "icon-text":
145
+ case "two-line":
146
+ case "link":
147
+ return "badge"
148
+ case "badge-array":
149
+ return "array"
150
+ case "boolean":
151
+ return "boolean"
152
+ default:
153
+ return "text"
154
+ }
155
+ }
156
+
157
+ const STARRED_PATTERNS = ["isStarred", "starred", "favorite", "isFavorite", "pinned"]
158
+ const READ_PATTERNS = ["isRead", "read", "seen", "viewed"]
159
+ const ATTACHMENT_PATTERNS = ["hasAttachment", "hasAttachments", "attachments", "files"]
160
+ const PRIORITY_PATTERNS = ["priority", "urgency", "level", "importance"]
161
+
162
+ function pickField(sample: DynamicRecord, patterns: string[]): string | null {
163
+ for (const p of patterns) {
164
+ if (p in sample) return p
165
+ }
166
+ return null
167
+ }
168
+
169
+ export function resolveInboxConfig(
170
+ data: DynamicRecord[],
171
+ user?: InboxConfig,
172
+ ): Required<Omit<InboxConfig, "titlePath" | "previewPath">> & Pick<InboxConfig, "titlePath" | "previewPath"> {
173
+ const sample = data?.[0] && typeof data[0] === "object" ? data[0] : {}
174
+
175
+ const auto = {
176
+ starredField: pickField(sample, STARRED_PATTERNS),
177
+ readField: pickField(sample, READ_PATTERNS),
178
+ attachmentField: pickField(sample, ATTACHMENT_PATTERNS),
179
+ priorityField: pickField(sample, PRIORITY_PATTERNS),
180
+ }
181
+
182
+ return {
183
+ starredField: user?.starredField !== undefined ? user.starredField : auto.starredField,
184
+ readField: user?.readField !== undefined ? user.readField : auto.readField,
185
+ attachmentField: user?.attachmentField !== undefined ? user.attachmentField : auto.attachmentField,
186
+ priorityField: user?.priorityField !== undefined ? user.priorityField : auto.priorityField,
187
+ titlePath: user?.titlePath,
188
+ previewPath: user?.previewPath,
189
+ }
190
+ }
191
+
192
+ export function readInboxField(
193
+ item: DynamicRecord,
194
+ field: string | null | undefined,
195
+ ): any {
196
+ if (!field) return undefined
197
+ return getByPath(item, field)
198
+ }
@@ -0,0 +1,364 @@
1
+ import type { ReactElement } from "react"
2
+ import { Badge } from "../../components/Badge"
3
+ import { Divider } from "../../components/Divider"
4
+ import { cn } from "../cn"
5
+ import type { DynamicRecord, DynamicColumnConfig } from "../../components/DataViews/types"
6
+
7
+ export type NestedFieldMetadata = {
8
+ key: string
9
+ label: string
10
+ type: 'object' | 'array' | 'primitive'
11
+ valueType?: 'string' | 'number' | 'boolean' | 'date'
12
+ isArrayOfObjects?: boolean
13
+ depth: number
14
+ }
15
+
16
+ export function isPlainObject(value: any): boolean {
17
+ return value !== null &&
18
+ typeof value === 'object' &&
19
+ !Array.isArray(value) &&
20
+ !(value instanceof Date) &&
21
+ Object.keys(value).length > 0
22
+ }
23
+
24
+ export function formatFieldName(fieldName: string): string {
25
+ if (fieldName.includes("_")) {
26
+ return fieldName
27
+ .split("_")
28
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
29
+ .join(" ")
30
+ }
31
+
32
+ return fieldName
33
+ .replace(/([A-Z])/g, " $1")
34
+ .replace(/^./, (str) => str.toUpperCase())
35
+ .trim()
36
+ }
37
+
38
+ export function analyzeNestedFields(
39
+ item: DynamicRecord,
40
+ visibleColumnIds: Set<string> = new Set(),
41
+ excludeKeys: string[] = ['id', 'isRead', 'isStarred', 'hasAttachment', 'priority']
42
+ ): NestedFieldMetadata[] {
43
+ const nestedFields: NestedFieldMetadata[] = []
44
+
45
+ Object.entries(item).forEach(([key, value]) => {
46
+ if (visibleColumnIds.has(key) || excludeKeys.includes(key)) {
47
+ return
48
+ }
49
+
50
+ if (isPlainObject(value)) {
51
+ nestedFields.push({
52
+ key,
53
+ label: formatFieldName(key),
54
+ type: 'object',
55
+ depth: 0
56
+ })
57
+ } else if (Array.isArray(value) && value.length > 0) {
58
+ nestedFields.push({
59
+ key,
60
+ label: formatFieldName(key),
61
+ type: 'array',
62
+ isArrayOfObjects: isPlainObject(value[0]),
63
+ depth: 0
64
+ })
65
+ }
66
+ })
67
+
68
+ return nestedFields
69
+ }
70
+
71
+ export function isCurrencyField(key: string): boolean {
72
+ const lowerKey = key.toLowerCase()
73
+ return lowerKey.includes('salary') ||
74
+ lowerKey.includes('price') ||
75
+ lowerKey.includes('cost') ||
76
+ lowerKey.includes('amount') ||
77
+ lowerKey.includes('pay') ||
78
+ lowerKey.includes('fee')
79
+ }
80
+
81
+ export function isRatingField(key: string): boolean {
82
+ const lowerKey = key.toLowerCase()
83
+ return lowerKey.includes('rating') ||
84
+ lowerKey.includes('score')
85
+ }
86
+
87
+ export function renderPrimitiveValue(
88
+ key: string,
89
+ value: any,
90
+ options: {
91
+ showLabel?: boolean
92
+ labelClassName?: string
93
+ valueClassName?: string
94
+ } = {}
95
+ ): ReactElement | null {
96
+ const {
97
+ showLabel = true,
98
+ labelClassName = "text-content-presentation-global-tertiary",
99
+ valueClassName = "text-content-presentation-global-primary"
100
+ } = options
101
+
102
+ if (value == null) return null
103
+
104
+ const label = showLabel ? formatFieldName(key) : null
105
+
106
+ if (typeof value === 'boolean') {
107
+ return (
108
+ <div className="flex items-center gap-2">
109
+ {label && <span className={labelClassName}>{label}:</span>}
110
+ <Badge
111
+ color={value ? "green" : "gray"}
112
+ badgeStyle={value ? "solid" : "subtle"}
113
+ label={value ? "Yes" : "No"}
114
+ size="XS"
115
+
116
+ />
117
+ </div>
118
+ )
119
+ }
120
+
121
+ if (typeof value === 'number') {
122
+ const isCurrency = isCurrencyField(key)
123
+ const isRating = isRatingField(key)
124
+
125
+ if (isRating && value <= 5) {
126
+ return (
127
+ <div className="flex items-center gap-2">
128
+ {label && <span className={labelClassName}>{label}:</span>}
129
+ <div className="flex items-center gap-2">
130
+ <div className="flex">
131
+ {[...Array(5)].map((_, i) => (
132
+ <span key={i} className={i < Math.floor(value) ? 'text-yellow-500' : 'text-gray-300'}>
133
+
134
+ </span>
135
+ ))}
136
+ </div>
137
+ <span className="font-semibold text-sm">{value}</span>
138
+ </div>
139
+ </div>
140
+ )
141
+ }
142
+
143
+ return (
144
+ <div className="flex items-center gap-2">
145
+ {label && <span className={labelClassName}>{label}:</span>}
146
+ <span className={cn(
147
+ valueClassName,
148
+ isCurrency && "font-semibold text-green-600"
149
+ )}>
150
+ {isCurrency ? `$${value.toLocaleString()}` : value.toLocaleString()}
151
+ </span>
152
+ </div>
153
+ )
154
+ }
155
+
156
+ return (
157
+ <div className="flex items-center gap-2">
158
+ {label && <span className={labelClassName}>{label}:</span>}
159
+ <span className={valueClassName}>{String(value)}</span>
160
+ </div>
161
+ )
162
+ }
163
+
164
+ export function renderArrayValue(
165
+ key: string,
166
+ value: any[],
167
+ options: {
168
+ showLabel?: boolean
169
+ maxItems?: number
170
+ renderItem?: (item: any, index: number) => ReactElement
171
+ } = {}
172
+ ): ReactElement | null {
173
+ const { showLabel = true, maxItems, renderItem } = options
174
+
175
+ if (!Array.isArray(value) || value.length === 0) return null
176
+
177
+ const label = showLabel ? formatFieldName(key) : null
178
+ const displayItems = maxItems ? value.slice(0, maxItems) : value
179
+
180
+ if (isPlainObject(value[0])) {
181
+ return (
182
+ <div className="space-y-2">
183
+ {label && (
184
+ <div className="text-xs font-semibold text-content-presentation-global-primary uppercase tracking-wide">
185
+ {label} ({value.length})
186
+ </div>
187
+ )}
188
+ <div className="space-y-2">
189
+ {displayItems.map((item: any, idx: number) => {
190
+ if (renderItem) {
191
+ return renderItem(item, idx)
192
+ }
193
+ return (
194
+ <div key={idx} className="p-2 rounded bg-background-presentation-form-field-primary">
195
+ {renderNestedObject(item, 1)}
196
+ </div>
197
+ )
198
+ })}
199
+ {maxItems && value.length > maxItems && (
200
+ <div className="text-xs text-content-presentation-global-tertiary">
201
+ +{value.length - maxItems} more
202
+ </div>
203
+ )}
204
+ </div>
205
+ </div>
206
+ )
207
+ }
208
+
209
+ return (
210
+ <div className="flex items-start gap-2">
211
+ {label && <span className="text-content-presentation-global-tertiary">{label}:</span>}
212
+ <div className="flex flex-wrap gap-1">
213
+ {displayItems.map((item: any, idx: number) => (
214
+ <Badge key={idx} color="blue" badgeStyle="solid" label={String(item)} size="XS" />
215
+ ))}
216
+ {maxItems && value.length > maxItems && (
217
+ <Badge color="gray" badgeStyle="subtle" label={`+${value.length - maxItems}`} size="XS" />
218
+ )}
219
+ </div>
220
+ </div>
221
+ )
222
+ }
223
+
224
+ export function renderNestedObject(
225
+ obj: any,
226
+ depth: number = 0,
227
+ options: {
228
+ maxDepth?: number
229
+ showSeparators?: boolean
230
+ } = {}
231
+ ): ReactElement {
232
+ const { maxDepth = 3 } = options
233
+
234
+ if (depth >= maxDepth) {
235
+ return <div className="text-xs text-content-presentation-global-tertiary">...</div>
236
+ }
237
+
238
+ return (
239
+ <div className="space-y-2 text-sm">
240
+ {Object.entries(obj).map(([key, value]) => {
241
+ if (isPlainObject(value)) {
242
+ return (
243
+ <div key={key} className="space-y-1">
244
+ <div className="text-xs font-semibold text-content-presentation-global-primary uppercase tracking-wide">
245
+ {formatFieldName(key)}
246
+ </div>
247
+ <div className="pl-3 border-l-2 border-border-presentation-global-primary">
248
+ {renderNestedObject(value, depth + 1, options)}
249
+ </div>
250
+ </div>
251
+ )
252
+ }
253
+
254
+ if (Array.isArray(value)) {
255
+ return <div key={key}>{renderArrayValue(key, value)}</div>
256
+ }
257
+
258
+ return <div key={key}>{renderPrimitiveValue(key, value)}</div>
259
+ })}
260
+ </div>
261
+ )
262
+ }
263
+
264
+ export function renderDetailView(
265
+ selectedItem: DynamicRecord,
266
+ visibleColumns: DynamicColumnConfig[],
267
+ renderCellValue: (value: any, column: DynamicColumnConfig, row: DynamicRecord) => React.ReactNode
268
+ ): ReactElement {
269
+ const visibleColumnIds = new Set(visibleColumns.map(col => col.id))
270
+ const nestedFields = analyzeNestedFields(selectedItem, visibleColumnIds)
271
+
272
+ return (
273
+ <div className="space-y-6">
274
+ {visibleColumns.slice(2).length > 0 && (
275
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
276
+ {visibleColumns.slice(2).map((col) => {
277
+ const value = selectedItem[col.id]
278
+ if (value == null && value !== 0 && value !== false) return null
279
+ return (
280
+ <div key={col.id} className="space-y-1">
281
+ <dt className="text-xs font-medium text-content-presentation-global-tertiary uppercase tracking-wide">
282
+ {col.label}
283
+ </dt>
284
+ <dd className="text-sm text-content-presentation-global-primary">
285
+ {renderCellValue(value, col, selectedItem)}
286
+ </dd>
287
+ </div>
288
+ )
289
+ })}
290
+ </div>
291
+ )}
292
+
293
+ {nestedFields.map((field, index) => {
294
+ const value = selectedItem[field.key]
295
+
296
+ if (field.type === 'object' && isPlainObject(value)) {
297
+ return (
298
+ <div key={field.key}>
299
+ {index > 0 && <Divider />}
300
+ <div>
301
+ <h3 className="text-sm font-semibold text-content-presentation-global-primary mb-3">
302
+ {field.label}
303
+ </h3>
304
+ {renderNestedObject(value)}
305
+ </div>
306
+ </div>
307
+ )
308
+ }
309
+
310
+ if (field.type === 'array' && Array.isArray(value)) {
311
+ return (
312
+ <div key={field.key}>
313
+ {index > 0 && <Divider />}
314
+ <div>
315
+ <h3 className="text-sm font-semibold text-content-presentation-global-primary mb-3">
316
+ {field.label}
317
+ </h3>
318
+ {renderArrayValue(field.key, value, { showLabel: false })}
319
+ </div>
320
+ </div>
321
+ )
322
+ }
323
+
324
+ return null
325
+ })}
326
+ </div>
327
+ )
328
+ }
329
+
330
+ export function getDataStructureSummary(data: DynamicRecord[]): {
331
+ totalFields: number
332
+ nestedFields: string[]
333
+ arrayFields: string[]
334
+ primitiveFields: string[]
335
+ } {
336
+ if (!data || data.length === 0) {
337
+ return { totalFields: 0, nestedFields: [], arrayFields: [], primitiveFields: [] }
338
+ }
339
+
340
+ const allKeys = new Set<string>()
341
+ const nestedKeys = new Set<string>()
342
+ const arrayKeys = new Set<string>()
343
+ const primitiveKeys = new Set<string>()
344
+
345
+ data.forEach(item => {
346
+ Object.entries(item).forEach(([key, value]) => {
347
+ allKeys.add(key)
348
+ if (isPlainObject(value)) {
349
+ nestedKeys.add(key)
350
+ } else if (Array.isArray(value)) {
351
+ arrayKeys.add(key)
352
+ } else {
353
+ primitiveKeys.add(key)
354
+ }
355
+ })
356
+ })
357
+
358
+ return {
359
+ totalFields: allKeys.size,
360
+ nestedFields: Array.from(nestedKeys),
361
+ arrayFields: Array.from(arrayKeys),
362
+ primitiveFields: Array.from(primitiveKeys)
363
+ }
364
+ }
@@ -0,0 +1,132 @@
1
+ export type Path = string
2
+
3
+ const PATH_CACHE = new Map<string, string[]>()
4
+
5
+ function splitPath(path: Path): string[] {
6
+ const cached = PATH_CACHE.get(path)
7
+ if (cached) return cached
8
+ const parts = path.split(".")
9
+ PATH_CACHE.set(path, parts)
10
+ return parts
11
+ }
12
+
13
+ export function getByPath(obj: unknown, path: Path | undefined | null): any {
14
+ if (obj == null || path == null || path === "") return undefined
15
+ if (typeof obj !== "object") return undefined
16
+
17
+ const parts = splitPath(path)
18
+ let cur: any = obj
19
+ for (let i = 0; i < parts.length; i++) {
20
+ if (cur == null) return undefined
21
+ cur = cur[parts[i]]
22
+ }
23
+ return cur
24
+ }
25
+
26
+ export function setByPath<T extends Record<string, any>>(
27
+ obj: T,
28
+ path: Path,
29
+ value: any,
30
+ ): T {
31
+ if (!path) return obj
32
+ const parts = splitPath(path)
33
+ if (parts.length === 0) return obj
34
+
35
+ const root: any = Array.isArray(obj) ? [...obj] : { ...obj }
36
+ let cur: any = root
37
+
38
+ for (let i = 0; i < parts.length - 1; i++) {
39
+ const key = parts[i]
40
+ const next = cur[key]
41
+ const cloned = next != null && typeof next === "object" && !Array.isArray(next)
42
+ ? { ...next }
43
+ : Array.isArray(next)
44
+ ? [...next]
45
+ : {}
46
+ cur[key] = cloned
47
+ cur = cloned
48
+ }
49
+
50
+ cur[parts[parts.length - 1]] = value
51
+ return root
52
+ }
53
+
54
+ export function hasPath(obj: unknown, path: Path | undefined | null): boolean {
55
+ return getByPath(obj, path) !== undefined
56
+ }
57
+
58
+ export function formatPathLabel(path: Path): string {
59
+ if (!path) return ""
60
+ const tail = path.includes(".") ? path.split(".").pop()! : path
61
+
62
+ if (tail.includes("_")) {
63
+ return tail
64
+ .split("_")
65
+ .filter(Boolean)
66
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
67
+ .join(" ")
68
+ }
69
+
70
+ return tail
71
+ .replace(/([A-Z])/g, " $1")
72
+ .replace(/^./, (s) => s.toUpperCase())
73
+ .trim()
74
+ }
75
+
76
+ export function findFirstDefined(data: any[], path: Path): any {
77
+ for (const item of data) {
78
+ const v = getByPath(item, path)
79
+ if (v != null) return v
80
+ }
81
+ return undefined
82
+ }
83
+
84
+ export function matchesFilterValues(
85
+ item: unknown,
86
+ path: Path,
87
+ filter: string[] | { kind: "number"; min?: number; max?: number } | { kind: "date"; from?: string; to?: string } | undefined,
88
+ ): boolean {
89
+ if (filter == null) return true
90
+
91
+ if (Array.isArray(filter)) {
92
+ if (filter.length === 0) return true
93
+ const value = getByPath(item, path)
94
+ if (Array.isArray(value)) {
95
+ const set = new Set(value.map((v) => String(v)))
96
+ return filter.some((s) => set.has(s))
97
+ }
98
+ if (typeof value === "boolean") {
99
+ return filter.includes(value ? "true" : "false")
100
+ }
101
+ return filter.includes(String(value ?? ""))
102
+ }
103
+
104
+ if (filter.kind === "number") {
105
+ if (filter.min == null && filter.max == null) return true
106
+ const raw = getByPath(item, path)
107
+ const n = typeof raw === "number" ? raw : Number(raw)
108
+ if (!Number.isFinite(n)) return false
109
+ if (filter.min != null && n < filter.min) return false
110
+ if (filter.max != null && n > filter.max) return false
111
+ return true
112
+ }
113
+
114
+ if (filter.kind === "date") {
115
+ if (filter.from == null && filter.to == null) return true
116
+ const raw = getByPath(item, path)
117
+ if (raw == null) return false
118
+ const ms = raw instanceof Date ? raw.getTime() : Date.parse(String(raw))
119
+ if (!Number.isFinite(ms)) return false
120
+ if (filter.from != null) {
121
+ const fromMs = Date.parse(filter.from)
122
+ if (Number.isFinite(fromMs) && ms < fromMs) return false
123
+ }
124
+ if (filter.to != null) {
125
+ const toMs = Date.parse(filter.to)
126
+ if (Number.isFinite(toMs) && ms > toMs + 24 * 60 * 60 * 1000 - 1) return false
127
+ }
128
+ return true
129
+ }
130
+
131
+ return true
132
+ }