@firecms/core 3.3.0 → 3.4.0-canary.2575a08

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 (155) hide show
  1. package/dist/app/useApp.d.ts +1 -0
  2. package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
  3. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  4. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  5. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
  7. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  8. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  9. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  10. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  11. package/dist/components/EntityPreview.d.ts +3 -1
  12. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  13. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  14. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  15. package/dist/components/common/table_url_params.d.ts +24 -0
  16. package/dist/components/common/useDataSourceTableController.d.ts +9 -1
  17. package/dist/components/common/useTableSearchHelper.d.ts +9 -1
  18. package/dist/core/DefaultDrawer.d.ts +10 -3
  19. package/dist/core/EntityEditView.d.ts +9 -1
  20. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  21. package/dist/core/field_configs.d.ts +1 -1
  22. package/dist/form/EntityForm.d.ts +8 -1
  23. package/dist/form/EntityFormActions.d.ts +2 -0
  24. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
  25. package/dist/form/index.d.ts +1 -0
  26. package/dist/hooks/data/delete.d.ts +1 -1
  27. package/dist/hooks/data/save.d.ts +1 -1
  28. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  29. package/dist/hooks/data/useEntityFetch.d.ts +9 -1
  30. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  31. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  32. package/dist/index.es.js +3658 -1820
  33. package/dist/index.es.js.map +1 -1
  34. package/dist/index.umd.js +3655 -1817
  35. package/dist/index.umd.js.map +1 -1
  36. package/dist/locales/pl.d.ts +2 -0
  37. package/dist/preview/index.d.ts +1 -0
  38. package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
  39. package/dist/types/collections.d.ts +31 -0
  40. package/dist/types/datasource.d.ts +67 -4
  41. package/dist/types/entities.d.ts +21 -1
  42. package/dist/types/entity_actions.d.ts +6 -0
  43. package/dist/types/entity_callbacks.d.ts +21 -0
  44. package/dist/types/navigation.d.ts +34 -5
  45. package/dist/types/plugins.d.ts +13 -0
  46. package/dist/types/properties.d.ts +17 -1
  47. package/dist/types/side_entity_controller.d.ts +7 -0
  48. package/dist/types/translations.d.ts +6 -0
  49. package/dist/util/__tests__/collections.test.d.ts +1 -0
  50. package/dist/util/__tests__/urls.test.d.ts +1 -0
  51. package/dist/util/collections.d.ts +1 -1
  52. package/dist/util/entities.d.ts +1 -0
  53. package/dist/util/entity_cache.d.ts +11 -0
  54. package/dist/util/geopoint.d.ts +22 -0
  55. package/dist/util/index.d.ts +2 -0
  56. package/dist/util/navigation_blocking.d.ts +22 -0
  57. package/dist/util/navigation_from_path.d.ts +17 -0
  58. package/dist/util/navigation_utils.d.ts +107 -3
  59. package/dist/util/parent_references_from_path.d.ts +16 -0
  60. package/dist/util/paths.d.ts +28 -0
  61. package/dist/util/permissions.d.ts +10 -4
  62. package/dist/util/urls.d.ts +22 -0
  63. package/package.json +17 -10
  64. package/src/app/Scaffold.tsx +34 -44
  65. package/src/app/useApp.tsx +1 -0
  66. package/src/components/DeleteEntityDialog.tsx +2 -0
  67. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  68. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
  69. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  70. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  71. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  72. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  73. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  74. package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
  75. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  76. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -1
  77. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  78. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  79. package/src/components/EntityPreview.tsx +45 -41
  80. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  81. package/src/components/ReferenceWidget.tsx +1 -1
  82. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  83. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  84. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  85. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  86. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  87. package/src/components/common/default_entity_actions.tsx +22 -2
  88. package/src/components/common/table_url_params.ts +172 -0
  89. package/src/components/common/useDataSourceTableController.tsx +126 -163
  90. package/src/components/common/useTableSearchHelper.ts +26 -2
  91. package/src/contexts/SnackbarProvider.tsx +14 -1
  92. package/src/core/DefaultDrawer.tsx +150 -64
  93. package/src/core/DrawerNavigationGroup.tsx +27 -29
  94. package/src/core/DrawerNavigationItem.tsx +10 -12
  95. package/src/core/EntityEditView.tsx +77 -35
  96. package/src/core/EntityEditViewFormActions.tsx +3 -2
  97. package/src/core/EntitySidePanel.tsx +8 -4
  98. package/src/core/field_configs.tsx +15 -0
  99. package/src/form/EntityForm.tsx +31 -10
  100. package/src/form/EntityFormActions.tsx +2 -0
  101. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  102. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  103. package/src/form/index.tsx +1 -0
  104. package/src/hooks/data/delete.ts +8 -0
  105. package/src/hooks/data/save.ts +6 -1
  106. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  107. package/src/hooks/data/useEntityFetch.tsx +35 -4
  108. package/src/hooks/useBuildNavigationController.tsx +23 -8
  109. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  110. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  111. package/src/i18n/FireCMSi18nProvider.tsx +50 -4
  112. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  113. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  114. package/src/internal/useBuildDataSource.ts +37 -15
  115. package/src/internal/useBuildSideEntityController.tsx +20 -5
  116. package/src/locales/de.ts +7 -2
  117. package/src/locales/en.ts +7 -2
  118. package/src/locales/es.ts +7 -2
  119. package/src/locales/fr.ts +7 -2
  120. package/src/locales/hi.ts +7 -2
  121. package/src/locales/it.ts +7 -2
  122. package/src/locales/pl.ts +735 -0
  123. package/src/locales/pt.ts +7 -2
  124. package/src/preview/PropertyPreview.tsx +12 -0
  125. package/src/preview/components/ReferencePreview.tsx +7 -3
  126. package/src/preview/components/StorageThumbnail.tsx +24 -2
  127. package/src/preview/index.ts +1 -0
  128. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  129. package/src/routes/FireCMSRoute.tsx +54 -27
  130. package/src/types/collections.ts +34 -0
  131. package/src/types/datasource.ts +67 -4
  132. package/src/types/entities.ts +24 -1
  133. package/src/types/entity_actions.tsx +6 -0
  134. package/src/types/entity_callbacks.ts +24 -0
  135. package/src/types/navigation.ts +35 -5
  136. package/src/types/plugins.tsx +13 -0
  137. package/src/types/properties.ts +17 -0
  138. package/src/types/side_entity_controller.tsx +7 -0
  139. package/src/types/translations.ts +6 -0
  140. package/src/util/__tests__/collections.test.ts +32 -0
  141. package/src/util/__tests__/urls.test.ts +131 -0
  142. package/src/util/collections.ts +1 -1
  143. package/src/util/entities.ts +16 -1
  144. package/src/util/entity_cache.ts +18 -0
  145. package/src/util/geopoint.ts +81 -0
  146. package/src/util/index.ts +2 -0
  147. package/src/util/navigation_blocking.ts +45 -0
  148. package/src/util/navigation_from_path.ts +47 -7
  149. package/src/util/navigation_utils.ts +244 -4
  150. package/src/util/parent_references_from_path.ts +45 -4
  151. package/src/util/paths.ts +40 -3
  152. package/src/util/permissions.ts +22 -10
  153. package/src/util/previews.ts +8 -1
  154. package/src/util/resolutions.ts +8 -0
  155. package/src/util/urls.ts +52 -0
@@ -8,7 +8,7 @@ import { EntityEditView, OnUpdateParams } from "./EntityEditView";
8
8
  import { useSideDialogContext } from "./SideDialogs";
9
9
  import { CloseIcon, IconButton, OpenInFullIcon } from "@firecms/ui";
10
10
  import { useLocation, useNavigate } from "react-router-dom";
11
- import { saveEntityToMemoryCache } from "../util/entity_cache";
11
+ import { entityCacheKey, saveEntityToMemoryCache } from "../util/entity_cache";
12
12
 
13
13
  /**
14
14
  * This is the component in charge of rendering the side dialog used
@@ -25,6 +25,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
25
25
  path,
26
26
  entityId,
27
27
  fullIdPath,
28
+ pathSegments,
28
29
  formProps,
29
30
  } = props;
30
31
 
@@ -59,6 +60,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
59
60
  if (params.status !== "existing") {
60
61
  sideEntityController.replace({
61
62
  path: params.path,
63
+ pathSegments: params.pathSegments ?? pathSegments,
62
64
  entityId: params.entityId,
63
65
  fullIdPath: props.fullIdPath,
64
66
  selectedTab: params.selectedTab,
@@ -75,10 +77,10 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
75
77
  }
76
78
 
77
79
  const parentCollectionIds = useMemo(() => {
78
- return navigationController.getParentCollectionIds(path);
80
+ return navigationController.getParentCollectionIds(path, pathSegments);
79
81
  }, [navigationController, path]);
80
82
 
81
- const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path);
83
+ const collection = props.collection ?? navigationController.getCollection(fullIdPath ?? path, false, pathSegments);
82
84
 
83
85
  useEffect(() => {
84
86
  function beforeunload(e: any) {
@@ -116,6 +118,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
116
118
  <EntityEditView
117
119
  {...props}
118
120
  fullIdPath={fullIdPath}
121
+ pathSegments={pathSegments}
119
122
  layout={"side_panel"}
120
123
  collection={collection as EntityCollection}
121
124
  parentCollectionIds={parentCollectionIds}
@@ -135,7 +138,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
135
138
  className="self-center"
136
139
  size={"smallest"}
137
140
  onClick={() => {
138
- const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
141
+ const key = (status === "new" || status === "copy") ? path + "#new" : entityCacheKey(path, entityId);
139
142
  saveEntityToMemoryCache(key, values);
140
143
  if (entityId)
141
144
  navigate(location.pathname + location.search);
@@ -152,6 +155,7 @@ export function EntitySidePanel(props: EntitySidePanelProps) {
152
155
  }) => {
153
156
  sideEntityController.replace({
154
157
  path,
158
+ pathSegments,
155
159
  entityId,
156
160
  fullIdPath: props.fullIdPath,
157
161
  selectedTab,
@@ -6,6 +6,7 @@ import {
6
6
  ArrayOfReferencesFieldBinding,
7
7
  BlockFieldBinding,
8
8
  DateTimeFieldBinding,
9
+ GeopointFieldBinding,
9
10
  KeyValueFieldBinding,
10
11
  MapFieldBinding,
11
12
  MultiSelectFieldBinding,
@@ -34,6 +35,7 @@ import {
34
35
  ListAltIcon,
35
36
  ListIcon,
36
37
  MailIcon,
38
+ LocationOnIcon,
37
39
  NumbersIcon, PersonIcon,
38
40
  RepeatIcon,
39
41
  ScheduleIcon,
@@ -273,6 +275,17 @@ export const DEFAULT_FIELD_CONFIGS: Record<string, PropertyConfig<any>> = {
273
275
  Field: DateTimeFieldBinding
274
276
  }
275
277
  },
278
+ geopoint: {
279
+ key: "geopoint",
280
+ name: "Geopoint",
281
+ description: "Latitude and longitude pair",
282
+ Icon: LocationOnIcon,
283
+ color: "#0ea5e9",
284
+ property: {
285
+ dataType: "geopoint",
286
+ Field: GeopointFieldBinding
287
+ }
288
+ },
276
289
  group: {
277
290
  key: "group",
278
291
  name: "Group",
@@ -419,6 +432,8 @@ export function getDefaultFieldId(property: Property | ResolvedProperty) {
419
432
  return "switch";
420
433
  } else if (property.dataType === "date") {
421
434
  return "date_time";
435
+ } else if (property.dataType === "geopoint") {
436
+ return "geopoint";
422
437
  } else if (property.dataType === "reference") {
423
438
  return "reference";
424
439
  }
@@ -48,6 +48,7 @@ import { useAnalyticsController } from "../hooks/useAnalyticsController";
48
48
  import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
49
49
  import { ValidationError } from "yup";
50
50
  import {
51
+ entityCacheKey,
51
52
  flattenKeys,
52
53
  getEntityFromCache,
53
54
  removeEntityFromCache,
@@ -64,6 +65,8 @@ export type OnUpdateParams = {
64
65
  entity: Entity<any>,
65
66
  status: EntityStatus,
66
67
  path: string,
68
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
69
+ pathSegments?: string[],
67
70
  entityId?: string;
68
71
  selectedTab?: string;
69
72
  collection: EntityCollection<any>
@@ -71,6 +74,11 @@ export type OnUpdateParams = {
71
74
 
72
75
  export type EntityFormProps<M extends Record<string, any>> = {
73
76
  path: string;
77
+ /**
78
+ * `path` split at its real segment boundaries; forwarded to the datasource for
79
+ * generateEntityId and checkUniqueField. Never derived from `path`.
80
+ */
81
+ pathSegments?: string[];
74
82
  fullIdPath?: string;
75
83
  collection: EntityCollection<M>;
76
84
  entityId?: string;
@@ -214,6 +222,7 @@ export function getChanges<T extends object>(source: Partial<T>, comparison: Par
214
222
 
215
223
  export function EntityForm<M extends Record<string, any>>({
216
224
  path,
225
+ pathSegments,
217
226
  fullIdPath,
218
227
  entityId: entityIdProp,
219
228
  collection,
@@ -270,6 +279,7 @@ export function EntityForm<M extends Record<string, any>>({
270
279
  entityId: entityIdProp,
271
280
  collection,
272
281
  path,
282
+ pathSegments,
273
283
  values: valuesToBeSaved
274
284
  });
275
285
  }, false, 2000);
@@ -292,7 +302,7 @@ export function EntityForm<M extends Record<string, any>>({
292
302
  if (mustSetCustomId) {
293
303
  return undefined;
294
304
  } else {
295
- return dataSource.generateEntityId(path, collection);
305
+ return dataSource.generateEntityId(path, collection, pathSegments);
296
306
  }
297
307
  } else {
298
308
  return entityIdProp;
@@ -308,7 +318,7 @@ export function EntityForm<M extends Record<string, any>>({
308
318
  const baseInitialValues = useMemo(() => getInitialEntityValues(authController, collection, path, status, entity, customizationController.propertyConfigs), [authController, collection, path, status, entity, customizationController.propertyConfigs]);
309
319
 
310
320
  const localChangesDataRaw = useMemo(() => entityId
311
- ? getEntityFromCache(path + "/" + entityId)
321
+ ? getEntityFromCache(entityCacheKey(path, entityId))
312
322
  : getEntityFromCache(path + "#new"), [entityId, path]);
313
323
 
314
324
  const [localChangesCleared, setLocalChangesCleared] = useState<boolean>(false);
@@ -387,8 +397,8 @@ export function EntityForm<M extends Record<string, any>>({
387
397
  onValuesModified?.(false, initialValues as M);
388
398
  },
389
399
  onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
390
- const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
391
- if (controller.dirty) {
400
+ const key = (status === "new" || status === "copy") ? path + "#new" : entityCacheKey(path, entityId);
401
+ if (controller.dirty && localChangesBackup !== false) {
392
402
  const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
393
403
  if (touchedValues && Object.keys(touchedValues).length > 0) {
394
404
  saveEntityToCache(key, touchedValues);
@@ -468,8 +478,8 @@ export function EntityForm<M extends Record<string, any>>({
468
478
  removeEntityFromMemoryCache(path + "#new");
469
479
  removeEntityFromCache(path + "#new");
470
480
  } else {
471
- removeEntityFromMemoryCache(path + "/" + entityId);
472
- removeEntityFromCache(path + "/" + entityId);
481
+ removeEntityFromMemoryCache(entityCacheKey(path, entityId));
482
+ removeEntityFromCache(entityCacheKey(path, entityId));
473
483
  }
474
484
  }
475
485
 
@@ -491,6 +501,7 @@ export function EntityForm<M extends Record<string, any>>({
491
501
  entity: updatedEntity,
492
502
  status,
493
503
  path,
504
+ pathSegments,
494
505
  entityId: updatedEntity.id,
495
506
  collection
496
507
  });
@@ -511,16 +522,20 @@ export function EntityForm<M extends Record<string, any>>({
511
522
  previousValues,
512
523
  entityId,
513
524
  collection,
514
- path
525
+ path,
526
+ pathSegments
515
527
  }: {
516
528
  collection: EntityCollection<M>,
517
529
  path: string,
530
+ /** `path` split at its real segment boundaries, travelling with the `path` above. */
531
+ pathSegments?: string[],
518
532
  entityId: string | undefined,
519
533
  values: M,
520
534
  previousValues?: M,
521
535
  }) => {
522
536
  return saveEntityWithCallbacks({
523
537
  path,
538
+ pathSegments,
524
539
  entityId,
525
540
  values,
526
541
  previousValues,
@@ -538,6 +553,7 @@ export function EntityForm<M extends Record<string, any>>({
538
553
  type EntityFormSaveParams<M extends Record<string, any>> = {
539
554
  collection: ResolvedEntityCollection<M>,
540
555
  path: string,
556
+ pathSegments?: string[],
541
557
  entityId: string | undefined,
542
558
  values: EntityValues<M>,
543
559
  previousValues?: EntityValues<M>,
@@ -547,6 +563,7 @@ export function EntityForm<M extends Record<string, any>>({
547
563
  const onSaveEntityRequest = async ({
548
564
  collection,
549
565
  path,
566
+ pathSegments,
550
567
  entityId,
551
568
  values,
552
569
  previousValues,
@@ -560,6 +577,7 @@ export function EntityForm<M extends Record<string, any>>({
560
577
  return saveEntity({
561
578
  collection,
562
579
  path,
580
+ pathSegments,
563
581
  entityId,
564
582
  values,
565
583
  previousValues
@@ -573,6 +591,7 @@ export function EntityForm<M extends Record<string, any>>({
573
591
  return onSaveEntityRequest({
574
592
  collection: resolvedCollection,
575
593
  path,
594
+ pathSegments,
576
595
  entityId,
577
596
  values,
578
597
  previousValues: entity?.values,
@@ -623,13 +642,14 @@ export function EntityForm<M extends Record<string, any>>({
623
642
  const plugins = customizationController.plugins;
624
643
 
625
644
  const actionsDisabled = disabled || formex.isSubmitting || (status === "existing" && !formex.dirty) || Boolean(disabledProp);
626
- const parentCollectionIds = navigationController.getParentCollectionIds(path);
645
+ const parentCollectionIds = navigationController.getParentCollectionIds(path, pathSegments);
627
646
 
628
647
  if (plugins && collection) {
629
648
  const actionProps: PluginFormActionProps = {
630
649
  entityId,
631
650
  parentCollectionIds,
632
651
  path,
652
+ pathSegments,
633
653
  status,
634
654
  collection,
635
655
  context,
@@ -692,7 +712,7 @@ export function EntityForm<M extends Record<string, any>>({
692
712
  const uniqueFieldValidator: CustomFieldValidator = useCallback(({
693
713
  name,
694
714
  value
695
- }) => dataSource.checkUniqueField(path, name, value, entityId, collection),
715
+ }) => dataSource.checkUniqueField(path, name, value, entityId, collection, pathSegments),
696
716
  [dataSource, path, entityId]);
697
717
 
698
718
  const validationSchema = useMemo(() => entityId
@@ -890,6 +910,7 @@ export function EntityForm<M extends Record<string, any>>({
890
910
  const dialogActions = <EntityFormActionsComponent
891
911
  collection={resolvedCollection}
892
912
  path={path}
913
+ pathSegments={pathSegments}
893
914
  fullPath={path}
894
915
  fullIdPath={fullIdPath}
895
916
  entity={entity}
@@ -924,7 +945,7 @@ export function EntityForm<M extends Record<string, any>>({
924
945
 
925
946
  {manualApplyLocalChanges && hasLocalChanges &&
926
947
  <LocalChangesMenu
927
- cacheKey={status === "new" || status === "copy" ? path + "#new" : path + "/" + entityId}
948
+ cacheKey={status === "new" || status === "copy" ? path + "#new" : entityCacheKey(path, entityId)}
928
949
  properties={resolvedCollection.properties}
929
950
  cachedData={localChangesDataRaw as Partial<M>}
930
951
  formex={formex}
@@ -26,6 +26,8 @@ export interface EntityFormActionsProps {
26
26
  fullIdPath?: string;
27
27
  collection: ResolvedEntityCollection;
28
28
  path: string;
29
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
30
+ pathSegments?: string[];
29
31
  entity?: Entity;
30
32
  layout: "bottom" | "side";
31
33
  savingError?: Error;
@@ -58,7 +58,7 @@ export function ArrayOfReferencesFieldBinding({
58
58
  }
59
59
 
60
60
  const onMultipleEntitiesSelected = useCallback((entities: Entity<any>[]) => {
61
- setValue(entities.map(e => getReferenceFrom(e)));
61
+ setValue(entities.filter(Boolean).map(e => getReferenceFrom(e)));
62
62
  }, [setValue]);
63
63
 
64
64
  const referenceDialogController = useReferenceDialog({
@@ -0,0 +1,139 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+
3
+ import { CloseIcon, IconButton, TextField } from "@firecms/ui";
4
+ import { FieldProps, GeoPoint } from "../../types";
5
+ import { FieldHelperText, LabelWithIcon } from "../components";
6
+ import { PropertyIdCopyTooltip } from "../../components";
7
+ import { useClearRestoreValue } from "../useClearRestoreValue";
8
+ import { getIconForProperty } from "../../util";
9
+ import { formatGeoPoint, getGeoPointCoordinates, parseGeoPoint } from "../../util/geopoint";
10
+
11
+ type GeopointFieldBindingProps = FieldProps<GeoPoint>;
12
+
13
+ export function GeopointFieldBinding({
14
+ propertyKey,
15
+ value,
16
+ setValue,
17
+ error,
18
+ showError,
19
+ disabled,
20
+ autoFocus,
21
+ property,
22
+ includeDescription,
23
+ size = "large"
24
+ }: GeopointFieldBindingProps) {
25
+
26
+ const coordinates = getGeoPointCoordinates(value);
27
+ const canClear = Boolean((property as any).clearable);
28
+ const [latitude, setLatitude] = useState<string>(coordinates ? coordinates.latitude.toString() : "");
29
+ const [longitude, setLongitude] = useState<string>(coordinates ? coordinates.longitude.toString() : "");
30
+ const [localError, setLocalError] = useState<string | undefined>();
31
+ const skipSyncRef = useRef(false);
32
+
33
+ useClearRestoreValue({
34
+ property,
35
+ value,
36
+ setValue
37
+ });
38
+
39
+ useEffect(() => {
40
+ if (skipSyncRef.current) {
41
+ skipSyncRef.current = false;
42
+ return;
43
+ }
44
+ const nextCoordinates = getGeoPointCoordinates(value);
45
+ setLatitude(nextCoordinates ? nextCoordinates.latitude.toString() : "");
46
+ setLongitude(nextCoordinates ? nextCoordinates.longitude.toString() : "");
47
+ }, [value]);
48
+
49
+ const updateGeoPoint = (nextLatitude: string, nextLongitude: string) => {
50
+ skipSyncRef.current = true;
51
+ setLatitude(nextLatitude);
52
+ setLongitude(nextLongitude);
53
+
54
+ const trimmedLatitude = nextLatitude.trim();
55
+ const trimmedLongitude = nextLongitude.trim();
56
+
57
+ if (!trimmedLatitude && !trimmedLongitude) {
58
+ setLocalError(undefined);
59
+ setValue(null);
60
+ return;
61
+ }
62
+
63
+ const parsed = parseGeoPoint(`${trimmedLatitude}, ${trimmedLongitude}`);
64
+
65
+ if (parsed.error) {
66
+ setLocalError(parsed.error);
67
+ setValue(null);
68
+ return;
69
+ }
70
+
71
+ setLocalError(undefined);
72
+ setValue(parsed.point);
73
+ };
74
+
75
+ const handleClear = (event?: React.MouseEvent) => {
76
+ if (event) {
77
+ event.preventDefault();
78
+ event.stopPropagation();
79
+ }
80
+ updateGeoPoint("", "");
81
+ };
82
+
83
+ const resolvedError = localError ?? error;
84
+ const shouldShowError = Boolean(resolvedError) || Boolean(showError && error);
85
+
86
+ return (
87
+ <>
88
+ <PropertyIdCopyTooltip propertyKey={propertyKey}>
89
+ <div className="flex flex-col gap-2">
90
+ <div className="mt-1">
91
+ <LabelWithIcon
92
+ icon={getIconForProperty(property, "small")}
93
+ required={property.validation?.required}
94
+ title={property.name}
95
+ className={shouldShowError ? "text-red-500 dark:text-red-500" : "text-text-secondary dark:text-text-secondary-dark"}
96
+ />
97
+ </div>
98
+ <div className="grid grid-cols-1 gap-2 md:grid-cols-2">
99
+ <TextField
100
+ size={size}
101
+ value={latitude}
102
+ onChange={(event) => updateGeoPoint(event.target.value, longitude)}
103
+ autoFocus={autoFocus}
104
+ label={"Latitude"}
105
+ type="number"
106
+ disabled={disabled}
107
+ endAdornment={canClear ? (
108
+ <IconButton onClick={handleClear}>
109
+ <CloseIcon />
110
+ </IconButton>
111
+ ) : undefined}
112
+ error={shouldShowError && Boolean(resolvedError)}
113
+ />
114
+ <TextField
115
+ size={size}
116
+ value={longitude}
117
+ onChange={(event) => updateGeoPoint(latitude, event.target.value)}
118
+ label={"Longitude"}
119
+ type="number"
120
+ disabled={disabled}
121
+ error={shouldShowError && Boolean(resolvedError)}
122
+ />
123
+ </div>
124
+ {value && !resolvedError && (
125
+ <div className="text-xs text-text-secondary dark:text-text-secondary-dark font-mono">
126
+ {formatGeoPoint(value)}
127
+ </div>
128
+ )}
129
+ </div>
130
+ </PropertyIdCopyTooltip>
131
+
132
+ <FieldHelperText includeDescription={includeDescription}
133
+ showError={shouldShowError}
134
+ error={resolvedError}
135
+ disabled={disabled}
136
+ property={property}/>
137
+ </>
138
+ );
139
+ }
@@ -11,6 +11,7 @@ export { StorageUploadFieldBinding } from "./field_bindings/StorageUploadFieldBi
11
11
  export { TextFieldBinding } from "./field_bindings/TextFieldBinding";
12
12
  export { SwitchFieldBinding } from "./field_bindings/SwitchFieldBinding";
13
13
  export { DateTimeFieldBinding } from "./field_bindings/DateTimeFieldBinding";
14
+ export { GeopointFieldBinding } from "./field_bindings/GeopointFieldBinding";
14
15
  export { ReferenceFieldBinding } from "./field_bindings/ReferenceFieldBinding";
15
16
  export { ReferenceAsStringFieldBinding } from "./field_bindings/ReferenceAsStringFieldBinding";
16
17
  export { MapFieldBinding } from "./field_bindings/MapFieldBinding";
@@ -46,6 +46,7 @@ export type DeleteEntityWithCallbacksProps<M extends Record<string, any>, USER e
46
46
  export async function deleteEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
47
47
  dataSource,
48
48
  entity,
49
+ pathSegments,
49
50
  collection,
50
51
  callbacks,
51
52
  onDeleteSuccess,
@@ -62,11 +63,17 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
62
63
 
63
64
  console.debug("Deleting entity", entity.path, entity.id);
64
65
 
66
+ // An explicit `pathSegments` wins, then the ones the entity was loaded with. The prop
67
+ // used to be accepted and silently ignored, which is the same class of bug this field
68
+ // exists to prevent.
69
+ const resolvedPathSegments = pathSegments ?? entity.pathSegments;
70
+
65
71
  const entityDeleteProps: EntityOnDeleteProps<M, any> = {
66
72
  entity,
67
73
  collection,
68
74
  entityId: entity.id,
69
75
  path: entity.path,
76
+ pathSegments: resolvedPathSegments,
70
77
  context
71
78
  };
72
79
 
@@ -81,6 +88,7 @@ export async function deleteEntityWithCallbacks<M extends Record<string, any>, U
81
88
  }
82
89
  }
83
90
  return dataSource.deleteEntity({
91
+ pathSegments: resolvedPathSegments,
84
92
  entity,
85
93
  collection
86
94
  }).then(() => {
@@ -43,6 +43,7 @@ export type SaveEntityWithCallbacksProps<M extends Record<string, any>> =
43
43
  export async function saveEntityWithCallbacks<M extends Record<string, any>, USER extends User>({
44
44
  collection,
45
45
  path,
46
+ pathSegments,
46
47
  entityId,
47
48
  values,
48
49
  previousValues,
@@ -67,7 +68,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
67
68
 
68
69
  const customizationController = context.customizationController;
69
70
 
70
- const resolvedPath = context.navigation.resolveIdsFrom(path);
71
+ const resolvedPath = context.navigation.resolveIdsFrom(path, pathSegments);
71
72
 
72
73
  const callbacks = collection.callbacks;
73
74
  if (callbacks?.onPreSave) {
@@ -83,6 +84,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
83
84
  updatedValues = await callbacks.onPreSave({
84
85
  collection: resolvedCollection,
85
86
  path,
87
+ pathSegments,
86
88
  resolvedPath,
87
89
  entityId,
88
90
  values,
@@ -103,6 +105,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
103
105
  return dataSource.saveEntity({
104
106
  collection,
105
107
  path: resolvedPath,
108
+ pathSegments,
106
109
  entityId,
107
110
  values: updatedValues,
108
111
  previousValues,
@@ -121,6 +124,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
121
124
  callbacks.onSaveSuccess({
122
125
  collection: resolvedCollection,
123
126
  path,
127
+ pathSegments,
124
128
  resolvedPath,
125
129
  entityId: entity.id,
126
130
  values: updatedValues,
@@ -150,6 +154,7 @@ export async function saveEntityWithCallbacks<M extends Record<string, any>, USE
150
154
  callbacks.onSaveFailure({
151
155
  collection: resolvedCollection,
152
156
  path,
157
+ pathSegments,
153
158
  resolvedPath,
154
159
  entityId,
155
160
  values: updatedValues,
@@ -13,6 +13,12 @@ export interface CollectionFetchProps<M extends Record<string, any>> {
13
13
  * Absolute collection path
14
14
  */
15
15
  path: string;
16
+ /**
17
+ * `path` split at its real segment boundaries. Forwarded to the datasource so a parent
18
+ * entity id containing "/" stays unambiguous. Never derived from `path` — see
19
+ * `pathSegments` on the datasource props.
20
+ */
21
+ pathSegments?: string[];
16
22
 
17
23
  /**
18
24
  * collection of the entity displayed by this collection
@@ -63,6 +69,7 @@ export interface CollectionFetchResult<M extends Record<string, any>> {
63
69
  export function useCollectionFetch<M extends Record<string, any>, USER extends User>(
64
70
  {
65
71
  path: inputPath,
72
+ pathSegments: inputPathSegments,
66
73
  collection,
67
74
  filterValues,
68
75
  sortBy,
@@ -73,7 +80,13 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
73
80
  const dataSource = useDataSource(collection);
74
81
  const navigationController = useNavigationController();
75
82
 
76
- const path = navigationController.resolveIdsFrom(inputPath);
83
+ // Resolved together so the two representations always describe the same chain.
84
+ // Segments are resolved, never derived: only collection ids are swapped for their
85
+ // real paths, and entity ids are carried through whole (see useEntityFetch).
86
+ const pathSegments = inputPathSegments
87
+ ? (navigationController.resolveSegmentsFrom?.(inputPathSegments) ?? inputPathSegments)
88
+ : undefined;
89
+ const path = navigationController.resolveIdsFrom(inputPath, inputPathSegments);
77
90
 
78
91
  const sortByProperty = sortBy ? sortBy[0] : undefined;
79
92
  const currentSort = sortBy ? sortBy[1] : undefined;
@@ -91,6 +104,10 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
91
104
  setDataLoading(true);
92
105
 
93
106
  const onEntitiesUpdate = async (entities: Entity<M>[]) => {
107
+ // See useEntityFetch: entities carry the segments they were loaded with.
108
+ if (pathSegments) {
109
+ entities = entities.map(e => e.pathSegments ? e : { ...e, pathSegments });
110
+ }
94
111
  if (collection.callbacks?.onFetch) {
95
112
  try {
96
113
  entities = await Promise.all(
@@ -98,6 +115,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
98
115
  collection.callbacks!.onFetch!({
99
116
  collection,
100
117
  path,
118
+ pathSegments,
101
119
  entity,
102
120
  context
103
121
  })));
@@ -124,6 +142,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
124
142
  if (dataSource.listenCollection) {
125
143
  return dataSource.listenCollection<M>({
126
144
  path,
145
+ pathSegments,
127
146
  collection,
128
147
  onUpdate: onEntitiesUpdate,
129
148
  onError,
@@ -137,6 +156,7 @@ export function useCollectionFetch<M extends Record<string, any>, USER extends U
137
156
  } else {
138
157
  dataSource.fetchCollection<M>({
139
158
  path,
159
+ pathSegments,
140
160
  collection,
141
161
  searchString,
142
162
  filter: filterValues,