@firecms/core 3.0.0-rc.2 → 3.0.0-rc.4

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 (52) hide show
  1. package/dist/components/HomePage/HomePageDnD.d.ts +2 -1
  2. package/dist/components/PropertyCollectionView.d.ts +23 -0
  3. package/dist/core/EntityEditView.d.ts +10 -4
  4. package/dist/form/EntityForm.d.ts +5 -2
  5. package/dist/form/PropertyFieldBinding.d.ts +1 -1
  6. package/dist/form/components/LocalChangesMenu.d.ts +11 -0
  7. package/dist/form/index.d.ts +2 -1
  8. package/dist/index.es.js +1307 -384
  9. package/dist/index.es.js.map +1 -1
  10. package/dist/index.umd.js +1306 -383
  11. package/dist/index.umd.js.map +1 -1
  12. package/dist/types/collections.d.ts +11 -0
  13. package/dist/types/fields.d.ts +8 -0
  14. package/dist/types/properties.d.ts +32 -6
  15. package/dist/util/collections.d.ts +1 -0
  16. package/dist/util/entity_cache.d.ts +6 -1
  17. package/dist/util/make_properties_editable.d.ts +1 -2
  18. package/dist/util/objects.d.ts +1 -0
  19. package/dist/util/useStorageUploadController.d.ts +1 -0
  20. package/package.json +6 -6
  21. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +47 -47
  22. package/src/components/EntityCollectionView/EntityCollectionView.tsx +6 -1
  23. package/src/components/EntityView.tsx +29 -40
  24. package/src/components/HomePage/DefaultHomePage.tsx +13 -9
  25. package/src/components/HomePage/HomePageDnD.tsx +140 -38
  26. package/src/components/PropertyCollectionView.tsx +329 -0
  27. package/src/components/SelectableTable/SelectableTable.tsx +0 -12
  28. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +2 -1
  29. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +0 -1
  30. package/src/core/EntityEditView.tsx +27 -14
  31. package/src/core/EntityEditViewFormActions.tsx +33 -18
  32. package/src/core/EntitySidePanel.tsx +9 -3
  33. package/src/form/EntityForm.tsx +173 -42
  34. package/src/form/EntityFormActions.tsx +30 -15
  35. package/src/form/PropertyFieldBinding.tsx +4 -4
  36. package/src/form/components/ErrorFocus.tsx +22 -29
  37. package/src/form/components/LocalChangesMenu.tsx +144 -0
  38. package/src/form/field_bindings/BlockFieldBinding.tsx +1 -0
  39. package/src/form/index.tsx +5 -1
  40. package/src/hooks/useBuildNavigationController.tsx +104 -31
  41. package/src/preview/property_previews/MapPropertyPreview.tsx +2 -2
  42. package/src/preview/property_previews/NumberPropertyPreview.tsx +2 -2
  43. package/src/types/collections.ts +12 -0
  44. package/src/types/fields.tsx +10 -0
  45. package/src/types/properties.ts +35 -6
  46. package/src/util/collections.ts +8 -0
  47. package/src/util/createFormexStub.tsx +4 -0
  48. package/src/util/entity_cache.ts +71 -52
  49. package/src/util/join_collections.ts +3 -3
  50. package/src/util/make_properties_editable.ts +0 -22
  51. package/src/util/objects.ts +40 -2
  52. 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: dndItems,
172
- setItems: setDndItems,
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
- const newState = cloneItemsForDnd(dndItems);
353
- const srcIdx = newState.findIndex((g) => g.name === activeCont);
354
- const tgtIdx = newState.findIndex((g) => g.name === overCont);
355
- if (srcIdx === -1 || tgtIdx === -1) return;
356
- const src = newState[srcIdx];
357
- const tgt = newState[tgtIdx];
358
- const idxInSrc = src.entries.findIndex((e) => e.url === activeIdNow);
359
- if (idxInSrc === -1) return;
360
- const [moved] = src.entries.splice(idxInSrc, 1);
361
- tgt.entries.push(moved);
362
- interimItemsRef.current = newState;
363
- setDndItems(newState);
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
- const activeCont = findDndContainer(activeIdNow);
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
- onPersist?.(newState);
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
- recentlyMovedToNewContainer.current &&
464
- interimItemsRef.current
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
- onCardMovedBetweenGroups?.(
470
- dndItems
471
- .flatMap((g) => g.entries)
472
- .find((e) => e.url === activeIdNow)!
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
- onPersist?.(updated); // <- ensure rename is saved
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
+ }
@@ -327,15 +327,3 @@ function createFilterField({
327
327
  );
328
328
  }
329
329
 
330
- function filterableProperty(property: ResolvedProperty, partOfArray = false): boolean {
331
- if (partOfArray) {
332
- return ["string", "number", "date", "reference"].includes(property.dataType);
333
- }
334
- if (property.dataType === "array") {
335
- if (property.of)
336
- return filterableProperty(property.of, true);
337
- else
338
- return false;
339
- }
340
- return ["string", "number", "boolean", "date", "reference", "array"].includes(property.dataType);
341
- }
@@ -55,7 +55,7 @@ export function DateTimeFilterField({
55
55
  }
56
56
 
57
57
  setOperation(op);
58
- setInternalValue(newValue === null ? undefined : 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>}