@firecms/core 3.0.0-rc.2 → 3.0.0-rc.3
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/dist/components/HomePage/HomePageDnD.d.ts +2 -1
- package/dist/components/PropertyCollectionView.d.ts +23 -0
- package/dist/core/EntityEditView.d.ts +10 -4
- package/dist/form/EntityForm.d.ts +5 -2
- package/dist/form/components/LocalChangesMenu.d.ts +11 -0
- package/dist/form/index.d.ts +2 -1
- package/dist/index.es.js +1288 -364
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +1287 -363
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +11 -0
- package/dist/types/properties.d.ts +32 -6
- package/dist/util/collections.d.ts +1 -0
- package/dist/util/entity_cache.d.ts +6 -1
- package/dist/util/make_properties_editable.d.ts +1 -2
- package/dist/util/objects.d.ts +1 -0
- package/dist/util/useStorageUploadController.d.ts +1 -0
- package/package.json +6 -6
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +47 -47
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +6 -1
- package/src/components/EntityView.tsx +29 -40
- package/src/components/HomePage/DefaultHomePage.tsx +13 -9
- package/src/components/HomePage/HomePageDnD.tsx +140 -38
- package/src/components/PropertyCollectionView.tsx +329 -0
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +0 -1
- package/src/core/EntityEditView.tsx +27 -14
- package/src/core/EntityEditViewFormActions.tsx +33 -18
- package/src/core/EntitySidePanel.tsx +9 -3
- package/src/form/EntityForm.tsx +173 -42
- package/src/form/EntityFormActions.tsx +30 -15
- package/src/form/components/ErrorFocus.tsx +22 -29
- package/src/form/components/LocalChangesMenu.tsx +144 -0
- package/src/form/index.tsx +5 -1
- package/src/hooks/useBuildNavigationController.tsx +104 -31
- package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
- package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
- package/src/types/collections.ts +12 -0
- package/src/types/properties.ts +35 -6
- package/src/util/collections.ts +8 -0
- package/src/util/createFormexStub.tsx +4 -0
- package/src/util/entity_cache.ts +71 -52
- package/src/util/join_collections.ts +3 -3
- package/src/util/make_properties_editable.ts +0 -22
- package/src/util/objects.ts +40 -2
- package/src/util/useStorageUploadController.tsx +71 -34
|
@@ -163,13 +163,9 @@ export function SortableNavigationGroup({
|
|
|
163
163
|
);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
/* ─────────────────────────────────────────────────────────── */
|
|
167
|
-
/* Main DnD hook */
|
|
168
|
-
|
|
169
|
-
/* ─────────────────────────────────────────────────────────── */
|
|
170
166
|
export function useHomePageDnd({
|
|
171
|
-
items
|
|
172
|
-
setItems
|
|
167
|
+
items,
|
|
168
|
+
setItems,
|
|
173
169
|
disabled,
|
|
174
170
|
onCardMovedBetweenGroups,
|
|
175
171
|
onGroupMoved,
|
|
@@ -191,6 +187,9 @@ export function useHomePageDnd({
|
|
|
191
187
|
onPersist?: (latest: { name: string; entries: NavigationEntry[] }[]) => void;
|
|
192
188
|
}) {
|
|
193
189
|
/* ---------------- local state ---------------- */
|
|
190
|
+
const dndItems = items;
|
|
191
|
+
const setDndItems = setItems;
|
|
192
|
+
|
|
194
193
|
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null);
|
|
195
194
|
const [activeIsGroup, setActiveIsGroup] = useState(false);
|
|
196
195
|
const [currentDraggingGroupId, setCurrentDraggingGroupId] =
|
|
@@ -200,6 +199,15 @@ export function useHomePageDnd({
|
|
|
200
199
|
const [dialogOpenForGroup, setDialogOpenForGroup] = useState<string | null>(null);
|
|
201
200
|
const [isHoveringNewGroupDropZone, setIsHoveringNewGroupDropZone] =
|
|
202
201
|
useState(false);
|
|
202
|
+
const [pendingNewGroupName, setPendingNewGroupName] = useState<string | null>(null);
|
|
203
|
+
const [stateBeforeNewGroup, setStateBeforeNewGroup] = useState<
|
|
204
|
+
{ name: string; entries: NavigationEntry[] }[] | null
|
|
205
|
+
>(null);
|
|
206
|
+
|
|
207
|
+
/* store the original state before any drag modifications */
|
|
208
|
+
const preDragItemsRef = useRef<
|
|
209
|
+
{ name: string; entries: NavigationEntry[] }[] | null
|
|
210
|
+
>(null);
|
|
203
211
|
|
|
204
212
|
/* store interim state for cross-group moves */
|
|
205
213
|
const interimItemsRef = useRef<
|
|
@@ -321,6 +329,9 @@ export function useHomePageDnd({
|
|
|
321
329
|
setDndKitActiveNode(active);
|
|
322
330
|
if (disabled) return;
|
|
323
331
|
|
|
332
|
+
// Capture the original state before any drag modifications
|
|
333
|
+
preDragItemsRef.current = cloneItemsForDnd(dndItems);
|
|
334
|
+
|
|
324
335
|
const isGroup = dndItems.some((g) => g.name === active.id);
|
|
325
336
|
if (!active.data.current) active.data.current = {};
|
|
326
337
|
active.data.current.type = isGroup ? "group" : "item";
|
|
@@ -349,18 +360,35 @@ export function useHomePageDnd({
|
|
|
349
360
|
|
|
350
361
|
if (overCont && activeCont !== overCont) {
|
|
351
362
|
recentlyMovedToNewContainer.current = true;
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
363
|
+
lastOverId.current = overIdNow;
|
|
364
|
+
|
|
365
|
+
// Update state for visual feedback during drag
|
|
366
|
+
setDndItems((current) => {
|
|
367
|
+
const newState = cloneItemsForDnd(current);
|
|
368
|
+
const srcIdx = newState.findIndex((g) => g.name === activeCont);
|
|
369
|
+
const tgtIdx = newState.findIndex((g) => g.name === overCont);
|
|
370
|
+
if (srcIdx === -1 || tgtIdx === -1) return current;
|
|
371
|
+
const src = newState[srcIdx];
|
|
372
|
+
const tgt = newState[tgtIdx];
|
|
373
|
+
const idxInSrc = src.entries.findIndex((e) => e.url === activeIdNow);
|
|
374
|
+
if (idxInSrc === -1) return current;
|
|
375
|
+
const [moved] = src.entries.splice(idxInSrc, 1);
|
|
376
|
+
|
|
377
|
+
// Calculate insertion position - SAME logic as handleDragEnd
|
|
378
|
+
const overIsContainer = overIdNow === overCont;
|
|
379
|
+
if (overIsContainer) {
|
|
380
|
+
tgt.entries.push(moved);
|
|
381
|
+
} else {
|
|
382
|
+
const overIdx = tgt.entries.findIndex((e) => e.url === overIdNow);
|
|
383
|
+
if (overIdx !== -1) {
|
|
384
|
+
tgt.entries.splice(overIdx, 0, moved);
|
|
385
|
+
} else {
|
|
386
|
+
tgt.entries.push(moved);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return newState;
|
|
391
|
+
});
|
|
364
392
|
} else if (activeCont === overCont) {
|
|
365
393
|
recentlyMovedToNewContainer.current = false;
|
|
366
394
|
}
|
|
@@ -396,11 +424,26 @@ export function useHomePageDnd({
|
|
|
396
424
|
}
|
|
397
425
|
/* ─── card move ─── */
|
|
398
426
|
else {
|
|
399
|
-
|
|
427
|
+
// CRITICAL: Find source container from ORIGINAL pre-drag state, not current (potentially stale) dndItems
|
|
428
|
+
const findContainerInState = (id: string, state: { name: string; entries: NavigationEntry[] }[]): string | undefined => {
|
|
429
|
+
const group = state.find((g) => g.name === id);
|
|
430
|
+
if (group) return group.name;
|
|
431
|
+
for (const g of state) {
|
|
432
|
+
if (g.entries.some((e) => e.url === id)) return g.name;
|
|
433
|
+
}
|
|
434
|
+
return undefined;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const sourceState = preDragItemsRef.current || dndItems;
|
|
438
|
+
const activeCont = findContainerInState(activeIdNow as string, sourceState);
|
|
439
|
+
const overCont = findDndContainer(overIdNow);
|
|
400
440
|
|
|
401
441
|
/* drop on new-group zone */
|
|
402
442
|
if (overIdNow === "new-group-drop-zone") {
|
|
403
443
|
if (activeCont) {
|
|
444
|
+
// Save current state before making changes
|
|
445
|
+
setStateBeforeNewGroup(cloneItemsForDnd(dndItems));
|
|
446
|
+
|
|
404
447
|
const newState = cloneItemsForDnd(dndItems);
|
|
405
448
|
const srcIdx = newState.findIndex((g) => g.name === activeCont);
|
|
406
449
|
if (srcIdx !== -1) {
|
|
@@ -421,8 +464,10 @@ export function useHomePageDnd({
|
|
|
421
464
|
name: tentative,
|
|
422
465
|
entries: [dragged]
|
|
423
466
|
});
|
|
467
|
+
|
|
468
|
+
// Update local state but DON'T persist yet
|
|
424
469
|
setDndItems(newState);
|
|
425
|
-
|
|
470
|
+
setPendingNewGroupName(tentative);
|
|
426
471
|
setDialogOpenForGroup(tentative);
|
|
427
472
|
onNewGroupDrop?.();
|
|
428
473
|
}
|
|
@@ -459,18 +504,59 @@ export function useHomePageDnd({
|
|
|
459
504
|
onPersist?.(newState);
|
|
460
505
|
}
|
|
461
506
|
}
|
|
462
|
-
} else if (
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
) {
|
|
466
|
-
onPersist?.(interimItemsRef.current);
|
|
467
|
-
}
|
|
507
|
+
} else if (overCont && activeCont !== overCont) {
|
|
508
|
+
// Card moved between different groups - use CLEAN pre-drag state
|
|
509
|
+
const finalState = cloneItemsForDnd(sourceState);
|
|
468
510
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
511
|
+
// Find target container from clean state too
|
|
512
|
+
const finalOverId = lastOverId.current || overIdNow;
|
|
513
|
+
const cleanOverCont = findContainerInState(finalOverId as string, sourceState) || overCont;
|
|
514
|
+
|
|
515
|
+
const srcIdx = finalState.findIndex((g) => g.name === activeCont);
|
|
516
|
+
const tgtIdx = finalState.findIndex((g) => g.name === cleanOverCont);
|
|
517
|
+
|
|
518
|
+
if (srcIdx !== -1 && tgtIdx !== -1) {
|
|
519
|
+
const src = finalState[srcIdx];
|
|
520
|
+
const tgt = finalState[tgtIdx];
|
|
521
|
+
const idxInSrc = src.entries.findIndex((e) => e.url === activeIdNow);
|
|
522
|
+
|
|
523
|
+
if (idxInSrc !== -1) {
|
|
524
|
+
// Remove from source
|
|
525
|
+
const [moved] = src.entries.splice(idxInSrc, 1);
|
|
526
|
+
|
|
527
|
+
// Calculate insertion position in target
|
|
528
|
+
const overIsContainer = finalOverId === cleanOverCont;
|
|
529
|
+
if (overIsContainer) {
|
|
530
|
+
tgt.entries.push(moved);
|
|
531
|
+
} else {
|
|
532
|
+
const overIdx = tgt.entries.findIndex((e) => e.url === finalOverId);
|
|
533
|
+
if (overIdx !== -1) {
|
|
534
|
+
tgt.entries.splice(overIdx, 0, moved);
|
|
535
|
+
} else {
|
|
536
|
+
tgt.entries.push(moved);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Remove empty source group if needed
|
|
541
|
+
if (src.entries.length === 0) {
|
|
542
|
+
finalState.splice(srcIdx, 1);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
setDndItems(finalState);
|
|
546
|
+
onPersist?.(finalState);
|
|
547
|
+
|
|
548
|
+
onCardMovedBetweenGroups?.(moved);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} else if (recentlyMovedToNewContainer.current) {
|
|
552
|
+
// This shouldn't happen but log it for debugging
|
|
553
|
+
console.error("Move between containers detected but conditions not met", {
|
|
554
|
+
activeCont,
|
|
555
|
+
overCont,
|
|
556
|
+
activeIdNow,
|
|
557
|
+
overIdNow
|
|
558
|
+
});
|
|
559
|
+
}
|
|
474
560
|
}
|
|
475
561
|
}
|
|
476
562
|
|
|
@@ -501,9 +587,29 @@ export function useHomePageDnd({
|
|
|
501
587
|
...updated[idx],
|
|
502
588
|
name: newName
|
|
503
589
|
};
|
|
504
|
-
|
|
590
|
+
|
|
591
|
+
// Persist after successful rename
|
|
592
|
+
onPersist?.(updated);
|
|
505
593
|
return updated;
|
|
506
594
|
});
|
|
595
|
+
|
|
596
|
+
// Clear all pending state
|
|
597
|
+
setPendingNewGroupName(null);
|
|
598
|
+
setStateBeforeNewGroup(null);
|
|
599
|
+
setDialogOpenForGroup(null);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
/* Handle dialog close without renaming */
|
|
603
|
+
const handleDialogClose = () => {
|
|
604
|
+
// If there's a pending new group that wasn't renamed, restore previous state
|
|
605
|
+
if (pendingNewGroupName && dialogOpenForGroup === pendingNewGroupName && stateBeforeNewGroup) {
|
|
606
|
+
// Restore the state from before the new group was created
|
|
607
|
+
setDndItems(stateBeforeNewGroup);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Clear all pending state
|
|
611
|
+
setPendingNewGroupName(null);
|
|
612
|
+
setStateBeforeNewGroup(null);
|
|
507
613
|
setDialogOpenForGroup(null);
|
|
508
614
|
};
|
|
509
615
|
|
|
@@ -535,15 +641,12 @@ export function useHomePageDnd({
|
|
|
535
641
|
dialogOpenForGroup,
|
|
536
642
|
setDialogOpenForGroup,
|
|
537
643
|
handleRenameGroup,
|
|
644
|
+
handleDialogClose,
|
|
538
645
|
isHoveringNewGroupDropZone,
|
|
539
646
|
setIsHoveringNewGroupDropZone
|
|
540
647
|
};
|
|
541
648
|
}
|
|
542
649
|
|
|
543
|
-
/* ─────────────────────────────────────────────────────────── */
|
|
544
|
-
/* New-group drop-zone component */
|
|
545
|
-
|
|
546
|
-
/* ─────────────────────────────────────────────────────────── */
|
|
547
650
|
export function NewGroupDropZone({
|
|
548
651
|
disabled,
|
|
549
652
|
setIsHovering
|
|
@@ -588,8 +691,7 @@ export function NewGroupDropZone({
|
|
|
588
691
|
isOver
|
|
589
692
|
? "bg-surface-accent-100 dark:bg-surface-accent-800 border-surface-300 dark:border-surface-600"
|
|
590
693
|
: "bg-surface-50 dark:bg-surface-900 border-surface-200 dark:border-surface-700"
|
|
591
|
-
)}
|
|
592
|
-
>
|
|
694
|
+
)}>
|
|
593
695
|
<div className="text-center p-4">
|
|
594
696
|
<span className="block font-medium text-sm">
|
|
595
697
|
Drop here to create a new group
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { defaultBorderMixin, Typography } from "@firecms/ui";
|
|
3
|
+
import { PreviewSize, PropertyPreview } from "../preview";
|
|
4
|
+
import { ResolvedProperties, ResolvedProperty } from "../types";
|
|
5
|
+
import { getValueInPath } from "../util";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a readable label for a path and resolve the property
|
|
9
|
+
* Supports map and array (including arrays of maps)
|
|
10
|
+
*/
|
|
11
|
+
export function buildPropertyLabelAndGetProperty(
|
|
12
|
+
properties: ResolvedProperties,
|
|
13
|
+
key: string
|
|
14
|
+
): { label: string; property: ResolvedProperty | undefined } {
|
|
15
|
+
if (!key) return {
|
|
16
|
+
label: "",
|
|
17
|
+
property: undefined
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Parse "a[0].b.c[2]" -> ["a", 0, "b", "c", 2]
|
|
21
|
+
const segments: (string | number)[] = [];
|
|
22
|
+
const re = /([^[.\]]+)|\[(\d+)\]/g;
|
|
23
|
+
let m: RegExpExecArray | null;
|
|
24
|
+
while ((m = re.exec(key)) !== null) {
|
|
25
|
+
if (m[1] !== undefined) segments.push(m[1]);
|
|
26
|
+
else if (m[2] !== undefined) segments.push(Number(m[2]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let currentProps: ResolvedProperties | undefined = properties;
|
|
30
|
+
let currentProp: ResolvedProperty | undefined;
|
|
31
|
+
let lastLabel = "";
|
|
32
|
+
|
|
33
|
+
const getArrayOfProp = (p?: ResolvedProperty): ResolvedProperty | undefined => {
|
|
34
|
+
if (!p || p.dataType !== "array") return undefined;
|
|
35
|
+
return Array.isArray(p.of) ? (p.of[0] as ResolvedProperty) : (p.of as ResolvedProperty | undefined);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
for (const seg of segments) {
|
|
39
|
+
if (typeof seg === "number") {
|
|
40
|
+
// Last segment label should be the index itself
|
|
41
|
+
lastLabel = `[${seg}]`;
|
|
42
|
+
|
|
43
|
+
// Move schema context into the array element
|
|
44
|
+
if (currentProp?.dataType === "array") {
|
|
45
|
+
currentProp = getArrayOfProp(currentProp);
|
|
46
|
+
if (currentProp?.dataType === "map" && currentProp.properties) {
|
|
47
|
+
currentProps = currentProp.properties as ResolvedProperties;
|
|
48
|
+
} else {
|
|
49
|
+
currentProps = undefined;
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
// Index without array schema context
|
|
53
|
+
currentProp = undefined;
|
|
54
|
+
currentProps = undefined;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// seg is a string key
|
|
60
|
+
if (currentProps && (currentProps as any)[seg]) {
|
|
61
|
+
const nextProp = (currentProps as any)[seg] as ResolvedProperty;
|
|
62
|
+
currentProp = nextProp;
|
|
63
|
+
// Last segment label should be the property name (or the raw key)
|
|
64
|
+
lastLabel = nextProp.name || String(seg);
|
|
65
|
+
|
|
66
|
+
if (nextProp.dataType === "map" && nextProp.properties) {
|
|
67
|
+
currentProps = nextProp.properties as ResolvedProperties;
|
|
68
|
+
} else if (nextProp.dataType === "array") {
|
|
69
|
+
// Keep array prop; the next segment (index) will step into its element schema
|
|
70
|
+
currentProps = undefined;
|
|
71
|
+
} else {
|
|
72
|
+
currentProps = undefined;
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Unknown key or no schema context
|
|
76
|
+
currentProp = undefined;
|
|
77
|
+
currentProps = undefined;
|
|
78
|
+
lastLabel = String(seg);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
label: lastLabel,
|
|
84
|
+
property: currentProp
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const pathEndsWithIndex = (p: string) => /\[\d+\]$/.test(p);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Improved simple layout for nested changes:
|
|
92
|
+
* - Map or Array-of-Map -> section header + indented rows
|
|
93
|
+
* - Leaf or Array-of-Primitives -> single row with label and value
|
|
94
|
+
*/
|
|
95
|
+
export const PropertyCollectionView = ({
|
|
96
|
+
data,
|
|
97
|
+
properties,
|
|
98
|
+
baseKey = "",
|
|
99
|
+
suppressHeader = false,
|
|
100
|
+
size = "small"
|
|
101
|
+
}: {
|
|
102
|
+
data: any;
|
|
103
|
+
properties: ResolvedProperties;
|
|
104
|
+
baseKey?: string;
|
|
105
|
+
suppressHeader?: boolean;
|
|
106
|
+
size?: PreviewSize;
|
|
107
|
+
}) => {
|
|
108
|
+
|
|
109
|
+
const isTopLevel = !!baseKey && !baseKey.includes(".") && !baseKey.includes("[");
|
|
110
|
+
|
|
111
|
+
// Arrays
|
|
112
|
+
if (Array.isArray(data)) {
|
|
113
|
+
const {
|
|
114
|
+
label: arrayLabel,
|
|
115
|
+
property
|
|
116
|
+
} = baseKey
|
|
117
|
+
? buildPropertyLabelAndGetProperty(properties, baseKey)
|
|
118
|
+
: {
|
|
119
|
+
label: "",
|
|
120
|
+
property: undefined as ResolvedProperty | undefined
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const ofProp = property?.dataType === "array"
|
|
124
|
+
? (Array.isArray(property.of) ? property.of[0] : property.of) as ResolvedProperty | undefined
|
|
125
|
+
: undefined;
|
|
126
|
+
|
|
127
|
+
const isArrayOfMaps = ofProp?.dataType === "map";
|
|
128
|
+
const isArrayOfPrimitives = property?.dataType === "array" && ofProp && ofProp.dataType !== "map";
|
|
129
|
+
|
|
130
|
+
// Array of primitives -> single row
|
|
131
|
+
if (baseKey && property && isArrayOfPrimitives) {
|
|
132
|
+
return (
|
|
133
|
+
<div
|
|
134
|
+
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
135
|
+
<div className="col-span-4 pr-2">
|
|
136
|
+
<Typography variant="caption"
|
|
137
|
+
color={"secondary"}
|
|
138
|
+
component={"span"}
|
|
139
|
+
className="break-words">
|
|
140
|
+
{arrayLabel}
|
|
141
|
+
</Typography>
|
|
142
|
+
</div>
|
|
143
|
+
<div className="col-span-8">
|
|
144
|
+
<PropertyPreview propertyKey={baseKey}
|
|
145
|
+
value={data}
|
|
146
|
+
property={property}
|
|
147
|
+
size={size}/>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Array of maps or unknown -> array header + combined item header (MapName [index]) then content
|
|
154
|
+
return (
|
|
155
|
+
<div className={`${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
156
|
+
{baseKey && arrayLabel && !suppressHeader && (
|
|
157
|
+
<Typography variant="caption"
|
|
158
|
+
color={"secondary"}
|
|
159
|
+
component={"span"}>
|
|
160
|
+
{arrayLabel}
|
|
161
|
+
</Typography>
|
|
162
|
+
)}
|
|
163
|
+
<div className={baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : ""}>
|
|
164
|
+
{data.map((item, index) => {
|
|
165
|
+
if (item === null || item === undefined) return null;
|
|
166
|
+
const currentKey = baseKey ? `${baseKey}[${index}]` : `[${index}]`;
|
|
167
|
+
|
|
168
|
+
// Combined header text
|
|
169
|
+
const itemHeader = isArrayOfMaps && ofProp?.name
|
|
170
|
+
? `${ofProp.name} [${index}]`
|
|
171
|
+
: `[${index}]`;
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div key={currentKey} className="py-1">
|
|
175
|
+
<Typography variant="caption"
|
|
176
|
+
color={"secondary"}
|
|
177
|
+
component={"span"}>
|
|
178
|
+
{itemHeader}
|
|
179
|
+
</Typography>
|
|
180
|
+
<div className={`pl-4 mt-1 border-l ${defaultBorderMixin}`}>
|
|
181
|
+
<PropertyCollectionView
|
|
182
|
+
data={item}
|
|
183
|
+
properties={properties}
|
|
184
|
+
baseKey={currentKey}
|
|
185
|
+
suppressHeader={true} // don’t repeat the inner map header
|
|
186
|
+
size={size}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
})}
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Objects (maps or plain objects)
|
|
198
|
+
if (typeof data === "object" && data !== null) {
|
|
199
|
+
const {
|
|
200
|
+
label,
|
|
201
|
+
property
|
|
202
|
+
} = baseKey
|
|
203
|
+
? buildPropertyLabelAndGetProperty(properties, baseKey)
|
|
204
|
+
: {
|
|
205
|
+
label: "",
|
|
206
|
+
property: undefined as ResolvedProperty | undefined
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Non-map leaf-like object -> single row
|
|
210
|
+
if (baseKey && (!property || property.dataType !== "map" || !property.properties)) {
|
|
211
|
+
if (!property) return null;
|
|
212
|
+
return (
|
|
213
|
+
<div
|
|
214
|
+
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
215
|
+
<div className="col-span-4 pr-2">
|
|
216
|
+
<Typography variant="caption"
|
|
217
|
+
color={"secondary"}
|
|
218
|
+
component={"span"}
|
|
219
|
+
className="break-words">
|
|
220
|
+
{label}
|
|
221
|
+
</Typography>
|
|
222
|
+
</div>
|
|
223
|
+
<div className="col-span-8">
|
|
224
|
+
<PropertyPreview propertyKey={baseKey}
|
|
225
|
+
value={data}
|
|
226
|
+
property={property}
|
|
227
|
+
size={size}/>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Map with defined properties -> show map header only if not suppressed
|
|
234
|
+
const showMapHeader =
|
|
235
|
+
baseKey &&
|
|
236
|
+
!suppressHeader &&
|
|
237
|
+
property?.dataType === "map" &&
|
|
238
|
+
(property.name || !pathEndsWithIndex(baseKey));
|
|
239
|
+
|
|
240
|
+
const headerText = property?.name || label;
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div className={`${isTopLevel ? "py-4" : "py-1"} ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
244
|
+
{showMapHeader && (
|
|
245
|
+
<Typography variant="caption"
|
|
246
|
+
color={"secondary"}
|
|
247
|
+
component={"span"}
|
|
248
|
+
>
|
|
249
|
+
{headerText}
|
|
250
|
+
</Typography>
|
|
251
|
+
)}
|
|
252
|
+
<div className={baseKey ? `pl-4 mt-1 border-l ${defaultBorderMixin}` : ""}>
|
|
253
|
+
{Object.entries(data).map(([key, value]) => {
|
|
254
|
+
if (value === null || value === undefined) return null;
|
|
255
|
+
const currentKey = baseKey ? `${baseKey}.${key}` : key;
|
|
256
|
+
return (
|
|
257
|
+
<PropertyCollectionView
|
|
258
|
+
key={currentKey}
|
|
259
|
+
data={value}
|
|
260
|
+
properties={properties}
|
|
261
|
+
baseKey={currentKey}
|
|
262
|
+
size={size}
|
|
263
|
+
/>
|
|
264
|
+
);
|
|
265
|
+
})}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Primitives
|
|
272
|
+
if (baseKey) {
|
|
273
|
+
const {
|
|
274
|
+
label,
|
|
275
|
+
property
|
|
276
|
+
} = buildPropertyLabelAndGetProperty(properties, baseKey);
|
|
277
|
+
if (!property) return null;
|
|
278
|
+
return (
|
|
279
|
+
<div
|
|
280
|
+
className={`grid grid-cols-12 gap-x-4 ${isTopLevel ? "py-4" : "py-2"} items-start ${isTopLevel ? `border-b ${defaultBorderMixin} last:border-b-0` : ""}`}>
|
|
281
|
+
<div className="col-span-4 pr-2">
|
|
282
|
+
<Typography variant="caption"
|
|
283
|
+
color={"secondary"}
|
|
284
|
+
component={"span"}
|
|
285
|
+
className="break-words">
|
|
286
|
+
{label}
|
|
287
|
+
</Typography>
|
|
288
|
+
</div>
|
|
289
|
+
<div className="col-span-8">
|
|
290
|
+
<PropertyPreview propertyKey={baseKey}
|
|
291
|
+
value={data}
|
|
292
|
+
property={property}
|
|
293
|
+
size={size}/>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return null;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export function buildDataFromPaths(values: object, paths: string[]): object {
|
|
303
|
+
const result = {};
|
|
304
|
+
paths.forEach(path => {
|
|
305
|
+
const value = getValueInPath(values, path);
|
|
306
|
+
if (value === undefined) return;
|
|
307
|
+
|
|
308
|
+
// lodash.set would be perfect here
|
|
309
|
+
const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".");
|
|
310
|
+
let current: any = result;
|
|
311
|
+
segments.forEach((segment, index) => {
|
|
312
|
+
if (index === segments.length - 1) {
|
|
313
|
+
current[segment] = value;
|
|
314
|
+
} else {
|
|
315
|
+
const nextSegment = segments[index + 1];
|
|
316
|
+
const isNextAnIndex = /^\d+$/.test(nextSegment);
|
|
317
|
+
if (!current[segment]) {
|
|
318
|
+
if (isNextAnIndex) {
|
|
319
|
+
current[segment] = [];
|
|
320
|
+
} else {
|
|
321
|
+
current[segment] = {};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
current = current[segment];
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
@@ -55,7 +55,7 @@ export function DateTimeFilterField({
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
setOperation(op);
|
|
58
|
-
setInternalValue(newValue
|
|
58
|
+
setInternalValue(newValue);
|
|
59
59
|
|
|
60
60
|
const hasNewValue = newValue !== null && Array.isArray(newValue)
|
|
61
61
|
? newValue.length > 0
|
|
@@ -96,6 +96,7 @@ export function DateTimeFilterField({
|
|
|
96
96
|
mode={mode}
|
|
97
97
|
size={"large"}
|
|
98
98
|
locale={locale}
|
|
99
|
+
disabled={internalValue === null}
|
|
99
100
|
value={internalValue ?? undefined}
|
|
100
101
|
onChange={(dateValue: Date | null) => {
|
|
101
102
|
updateFilter(operation, dateValue === null ? undefined : dateValue);
|
|
@@ -139,7 +139,6 @@ export function StringNumberFilterField({
|
|
|
139
139
|
updateFilter(operation, dataType === "number" ? parseInt(value as string) : value as string)
|
|
140
140
|
}}
|
|
141
141
|
endAdornment={internalValue && <IconButton
|
|
142
|
-
className="absolute right-2 top-3"
|
|
143
142
|
onClick={(e) => updateFilter(operation, undefined)}>
|
|
144
143
|
<CloseIcon/>
|
|
145
144
|
</IconButton>}
|