@firecms/core 3.4.0-canary.63041d0 → 3.4.0-canary.9408c41

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 (160) hide show
  1. package/README.md +4 -0
  2. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  3. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  4. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +3 -2
  5. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  7. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  8. package/dist/components/ErrorBoundary.d.ts +1 -3
  9. package/dist/components/LicenseBanner.d.ts +23 -0
  10. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  11. package/dist/components/common/useDataSourceTableController.d.ts +4 -2
  12. package/dist/components/common/useTableSearchHelper.d.ts +3 -1
  13. package/dist/components/index.d.ts +1 -0
  14. package/dist/contexts/LicenseStatusContext.d.ts +16 -0
  15. package/dist/core/EntityEditView.d.ts +2 -0
  16. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  17. package/dist/core/index.d.ts +1 -0
  18. package/dist/core/plugin_translations.d.ts +38 -0
  19. package/dist/core/pro_plugins.d.ts +40 -0
  20. package/dist/de-DMp6cIY1.js +702 -0
  21. package/dist/de-DMp6cIY1.js.map +1 -0
  22. package/dist/editor/extensions/Image.d.ts +11 -0
  23. package/dist/editor/index.d.ts +2 -1
  24. package/dist/editor/lazy_editor.d.ts +6 -0
  25. package/dist/editor-BSo_aS1I.js +4563 -0
  26. package/dist/editor-BSo_aS1I.js.map +1 -0
  27. package/dist/form/EntityForm.d.ts +8 -1
  28. package/dist/form/EntityFormActions.d.ts +2 -0
  29. package/dist/fr-Cckz4IkR.js +702 -0
  30. package/dist/fr-Cckz4IkR.js.map +1 -0
  31. package/dist/hi-CDCoIqbu.js +702 -0
  32. package/dist/hi-CDCoIqbu.js.map +1 -0
  33. package/dist/hooks/data/delete.d.ts +1 -1
  34. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  35. package/dist/hooks/data/useEntityFetch.d.ts +4 -1
  36. package/dist/hooks/index.d.ts +2 -0
  37. package/dist/hooks/useLicenseStatus.d.ts +16 -0
  38. package/dist/hooks/useProjectLog.d.ts +11 -6
  39. package/dist/hooks/useTranslation.d.ts +1 -1
  40. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  41. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  42. package/dist/index.es.js +3113 -10785
  43. package/dist/index.es.js.map +1 -1
  44. package/dist/index.umd.js +7939 -6836
  45. package/dist/index.umd.js.map +1 -1
  46. package/dist/it-BJCUqixj.js +702 -0
  47. package/dist/it-BJCUqixj.js.map +1 -0
  48. package/dist/pl-DgT_joFf.js +704 -0
  49. package/dist/pl-DgT_joFf.js.map +1 -0
  50. package/dist/pt-B3dN3x0X.js +704 -0
  51. package/dist/pt-B3dN3x0X.js.map +1 -0
  52. package/dist/types/analytics.d.ts +1 -1
  53. package/dist/types/collections.d.ts +7 -0
  54. package/dist/types/datasource.d.ts +25 -4
  55. package/dist/types/entities.d.ts +21 -1
  56. package/dist/types/entity_actions.d.ts +6 -0
  57. package/dist/types/entity_callbacks.d.ts +21 -0
  58. package/dist/types/firecms.d.ts +18 -1
  59. package/dist/types/firecms_context.d.ts +8 -0
  60. package/dist/types/index.d.ts +1 -0
  61. package/dist/types/license.d.ts +54 -0
  62. package/dist/types/navigation.d.ts +34 -5
  63. package/dist/types/plugins.d.ts +22 -1
  64. package/dist/types/side_entity_controller.d.ts +3 -2
  65. package/dist/types/translations.d.ts +35 -2
  66. package/dist/util/date_fns_locales.d.ts +18 -0
  67. package/dist/util/entity_cache.d.ts +11 -0
  68. package/dist/util/index.d.ts +1 -0
  69. package/dist/util/lazy_eager.d.ts +3 -3
  70. package/dist/util/navigation_utils.d.ts +84 -2
  71. package/dist/util/parent_references_from_path.d.ts +16 -0
  72. package/dist/util/paths.d.ts +28 -0
  73. package/dist/util/permissions.d.ts +10 -4
  74. package/package.json +5 -6
  75. package/src/app/Scaffold.tsx +7 -0
  76. package/src/components/DeleteEntityDialog.tsx +2 -0
  77. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  78. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  79. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  80. package/src/components/EntityCollectionView/EntityCollectionView.tsx +49 -16
  81. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  82. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
  83. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  84. package/src/components/EntityPreview.tsx +4 -1
  85. package/src/components/ErrorBoundary.tsx +12 -18
  86. package/src/components/LicenseBanner.tsx +209 -0
  87. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  88. package/src/components/common/default_entity_actions.tsx +22 -2
  89. package/src/components/common/useDataSourceTableController.tsx +32 -8
  90. package/src/components/common/useTableSearchHelper.ts +5 -0
  91. package/src/components/index.tsx +1 -0
  92. package/src/contexts/LicenseStatusContext.tsx +50 -0
  93. package/src/core/DefaultAppBar.tsx +28 -16
  94. package/src/core/EntityEditView.tsx +11 -12
  95. package/src/core/EntityEditViewFormActions.tsx +3 -2
  96. package/src/core/EntitySidePanel.tsx +6 -4
  97. package/src/core/FireCMS.tsx +73 -47
  98. package/src/core/SideDialogs.tsx +44 -1
  99. package/src/core/index.tsx +1 -0
  100. package/src/core/plugin_translations.ts +87 -0
  101. package/src/core/pro_plugins.ts +79 -0
  102. package/src/editor/components/SlashCommandMenu.tsx +4 -1
  103. package/src/editor/extensions/Image.ts +17 -1
  104. package/src/editor/hooks/useProseMirror.ts +5 -0
  105. package/src/editor/index.ts +11 -1
  106. package/src/editor/lazy_editor.tsx +33 -0
  107. package/src/form/EntityForm.tsx +30 -9
  108. package/src/form/EntityFormActions.tsx +2 -0
  109. package/src/hooks/data/delete.ts +8 -0
  110. package/src/hooks/data/save.ts +5 -2
  111. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  112. package/src/hooks/data/useEntityFetch.tsx +28 -6
  113. package/src/hooks/index.tsx +2 -0
  114. package/src/hooks/useBuildNavigationController.tsx +70 -15
  115. package/src/hooks/useFireCMSContext.tsx +7 -3
  116. package/src/hooks/useLicenseStatus.tsx +32 -0
  117. package/src/hooks/useProjectLog.tsx +27 -9
  118. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  119. package/src/hooks/useTranslation.ts +3 -1
  120. package/src/i18n/FireCMSi18nProvider.tsx +199 -25
  121. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  122. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  123. package/src/internal/useBuildDataSource.ts +26 -15
  124. package/src/internal/useBuildSideEntityController.tsx +13 -5
  125. package/src/locales/de.ts +18 -2
  126. package/src/locales/en.ts +21 -3
  127. package/src/locales/es.ts +18 -2
  128. package/src/locales/fr.ts +18 -2
  129. package/src/locales/hi.ts +18 -2
  130. package/src/locales/it.ts +18 -2
  131. package/src/locales/pl.ts +18 -2
  132. package/src/locales/pt.ts +18 -2
  133. package/src/preview/components/DatePreview.tsx +2 -4
  134. package/src/preview/components/ReferencePreview.tsx +7 -3
  135. package/src/routes/FireCMSRoute.tsx +9 -4
  136. package/src/types/analytics.ts +3 -0
  137. package/src/types/collections.ts +8 -0
  138. package/src/types/datasource.ts +25 -4
  139. package/src/types/entities.ts +24 -1
  140. package/src/types/entity_actions.tsx +6 -0
  141. package/src/types/entity_callbacks.ts +24 -0
  142. package/src/types/firecms.tsx +19 -1
  143. package/src/types/firecms_context.tsx +9 -0
  144. package/src/types/index.ts +1 -0
  145. package/src/types/license.ts +62 -0
  146. package/src/types/navigation.ts +35 -5
  147. package/src/types/plugins.tsx +23 -1
  148. package/src/types/side_entity_controller.tsx +3 -2
  149. package/src/types/translations.ts +35 -2
  150. package/src/util/date_fns_locales.ts +168 -0
  151. package/src/util/entities.ts +3 -1
  152. package/src/util/entity_cache.ts +18 -0
  153. package/src/util/index.ts +1 -0
  154. package/src/util/lazy_eager.tsx +30 -8
  155. package/src/util/navigation_from_path.ts +6 -1
  156. package/src/util/navigation_utils.ts +210 -3
  157. package/src/util/parent_references_from_path.ts +42 -3
  158. package/src/util/paths.ts +40 -3
  159. package/src/util/permissions.ts +22 -10
  160. package/src/util/useStorageUploadController.tsx +6 -1
@@ -90,6 +90,7 @@ import { useSelectionController } from "./useSelectionController";
90
90
  import { EntityCollectionViewStartActions } from "./EntityCollectionViewStartActions";
91
91
  import { addRecentId, getRecentIds } from "./utils";
92
92
  import { useScrollRestoration } from "../common/useScrollRestoration";
93
+ import { entityCacheKey } from "../../util/entity_cache";
93
94
 
94
95
  const DEFAULT_ENTITY_OPEN_MODE: "side_panel" | "full_screen" = "side_panel";
95
96
 
@@ -109,8 +110,9 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
109
110
  /**
110
111
  * `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
111
112
  * Forwarded to the datasource so a parent entity id containing "/" stays unambiguous.
112
- * Defaults to splitting `fullPath`, which is correct for any backend whose ids cannot
113
- * contain "/".
113
+ *
114
+ * Optional, and never derived by splitting `fullPath` — a guess would be wrong in
115
+ * exactly the case the field exists for. Absent means "not known here", not "no slashes".
114
116
  */
115
117
  pathSegments?: string[];
116
118
  /**
@@ -198,7 +200,7 @@ export const EntityCollectionView = React.memo(
198
200
  collectionRef.current = collection;
199
201
  }, [collection]);
200
202
 
201
- const canCreateEntities = canCreateEntity(collection, authController, fullPath, null);
203
+ const canCreateEntities = canCreateEntity(collection, authController, fullPath, null, pathSegments);
202
204
  const [highlightedEntity, setHighlightedEntity] = useState<Entity<M> | undefined>(undefined);
203
205
  const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
204
206
 
@@ -228,7 +230,7 @@ export const EntityCollectionView = React.memo(
228
230
 
229
231
  const checkInlineEditing = useCallback((entity?: Entity<any>): boolean => {
230
232
  const collection = collectionRef.current;
231
- if (!canEditEntity(collection, authController, fullPath, entity ?? null)) {
233
+ if (!canEditEntity(collection, authController, fullPath, entity ?? null, pathSegments)) {
232
234
  return false;
233
235
  }
234
236
  return collection.inlineEditing === undefined || collection.inlineEditing;
@@ -351,10 +353,16 @@ export const EntityCollectionView = React.memo(
351
353
  addRecentId(collection.id, clickedEntity.id);
352
354
  }
353
355
 
354
- const path = collection?.collectionGroup ? clickedEntity.path : (fullPath ?? clickedEntity.path);
356
+ // In a collection group the row's own path is the real one, so the row's segments
357
+ // are the matching ones; otherwise both come from this view. `path` and
358
+ // `pathSegments` are picked from the same source so they cannot describe
359
+ // different chains.
360
+ const isCollectionGroup = Boolean(collection?.collectionGroup);
361
+ const path = isCollectionGroup ? clickedEntity.path : (fullPath ?? clickedEntity.path);
355
362
  navigateToEntity({
356
363
  navigation,
357
364
  path,
365
+ pathSegments: isCollectionGroup ? clickedEntity.pathSegments : (pathSegments ?? clickedEntity.pathSegments),
358
366
  fullIdPath,
359
367
  sideEntityController,
360
368
  openEntityMode,
@@ -461,7 +469,7 @@ export const EntityCollectionView = React.memo(
461
469
  }
462
470
  }, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController, viewMode]);
463
471
 
464
- const createEnabled = canCreateEntity(collection, authController, fullPath, null);
472
+ const createEnabled = canCreateEntity(collection, authController, fullPath, null, pathSegments);
465
473
 
466
474
  const uniqueFieldValidator: UniqueFieldValidator = useCallback(
467
475
  ({
@@ -469,8 +477,8 @@ export const EntityCollectionView = React.memo(
469
477
  value,
470
478
  property,
471
479
  entityId
472
- }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection),
473
- [fullPath]);
480
+ }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection, pathSegments),
481
+ [fullPath, pathSegments]);
474
482
 
475
483
  const onValueChange: OnCellValueChange<any, any> = ({
476
484
  value,
@@ -484,6 +492,8 @@ export const EntityCollectionView = React.memo(
484
492
 
485
493
  const saveProps: SaveEntityProps = {
486
494
  path: entity.path ?? fullPath,
495
+ // Paired with the `path` above: the row's segments describe the row's path.
496
+ pathSegments: entity.path ? entity.pathSegments : pathSegments,
487
497
  entityId: entity.id,
488
498
  values: updatedValues,
489
499
  previousValues: entity.values,
@@ -514,7 +524,7 @@ export const EntityCollectionView = React.memo(
514
524
 
515
525
  };
516
526
 
517
- const resolvedFullPath = navigation.resolveIdsFrom(fullPath);
527
+ const resolvedFullPath = navigation.resolveIdsFrom(fullPath, pathSegments);
518
528
  const resolvedCollection = useMemo(() => resolveCollection<M>({
519
529
  collection,
520
530
  path: fullPath,
@@ -676,13 +686,15 @@ export const EntityCollectionView = React.memo(
676
686
  width: 260,
677
687
  dependencies: [],
678
688
  Builder: ({ entity }) => {
679
- const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path);
689
+ // `entity.path` is a RAW datasource path, so its segments are passed:
690
+ // splitting it would mis-read any parent id containing "/".
691
+ const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path, entity.pathSegments);
680
692
  return (
681
693
  <div className={"flex flex-col gap-2 w-full"}>
682
694
  {collectionsWithPath.map((reference) => {
683
695
  return (
684
696
  <ReferencePreview
685
- key={reference.path + "/" + reference.id}
697
+ key={entityCacheKey(reference.path, reference.id)}
686
698
  reference={reference}
687
699
  size={"small"} />
688
700
  );
@@ -713,7 +725,7 @@ export const EntityCollectionView = React.memo(
713
725
  entity?: Entity<M>,
714
726
  customEntityActions?: EntityAction[]
715
727
  }): EntityAction[] => {
716
- const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity) : true;
728
+ const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity, entity.pathSegments ?? pathSegments) : true;
717
729
  const actions: EntityAction[] = [
718
730
  { ...editEntityAction, name: t("edit") }
719
731
  ];
@@ -789,6 +801,7 @@ export const EntityCollectionView = React.memo(
789
801
  // EntitiesCount fetches count and updates breadcrumb - no visual rendering needed here
790
802
  const countFetcher = <EntitiesCount
791
803
  fullPath={fullPath}
804
+ pathSegments={pathSegments}
792
805
  collection={collection}
793
806
  filter={tableController.filterValues}
794
807
  sortBy={tableController.sortBy}
@@ -842,6 +855,7 @@ export const EntityCollectionView = React.memo(
842
855
  onTextSearchClick,
843
856
  textSearchEnabled
844
857
  } = useTableSearchHelper({
858
+ pathSegments,
845
859
  collection,
846
860
  fullPath: resolvedFullPath,
847
861
  parentCollectionIds,
@@ -906,6 +920,7 @@ export const EntityCollectionView = React.memo(
906
920
  collection={collection}
907
921
  tableController={tableController}
908
922
  path={fullPath}
923
+ pathSegments={pathSegments}
909
924
  relativePath={collection.path}
910
925
  selectionController={usedSelectionController}
911
926
  collectionEntitiesCount={docsCount}
@@ -917,6 +932,7 @@ export const EntityCollectionView = React.memo(
917
932
  onMultipleDeleteClick={onMultipleDeleteClick}
918
933
  onNewClick={onNewClick}
919
934
  path={fullPath}
935
+ pathSegments={pathSegments}
920
936
  relativePath={collection.path}
921
937
  selectionController={usedSelectionController}
922
938
  selectionEnabled={selectionEnabled}
@@ -933,6 +949,7 @@ export const EntityCollectionView = React.memo(
933
949
  collection={collection}
934
950
  tableController={tableController}
935
951
  fullPath={fullPath}
952
+ pathSegments={pathSegments}
936
953
  parentCollectionIds={parentCollectionIds}
937
954
  columnProperty={selectedKanbanProperty}
938
955
  onEntityClick={onEntityClick}
@@ -1024,6 +1041,7 @@ export const EntityCollectionView = React.memo(
1024
1041
  getIdColumnWidth={getIdColumnWidth}
1025
1042
  additionalIDHeaderWidget={<EntityIdHeaderWidget
1026
1043
  path={fullPath}
1044
+ pathSegments={pathSegments}
1027
1045
  fullIdPath={fullIdPath ?? fullPath}
1028
1046
  collection={collection} />}
1029
1047
  openEntityMode={openEntityMode}
@@ -1063,6 +1081,7 @@ export const EntityCollectionView = React.memo(
1063
1081
 
1064
1082
  {popupCell && <PopupFormField
1065
1083
  key={`popup_form_${popupCell?.propertyKey}_${popupCell?.entityId}`}
1084
+ pathSegments={pathSegments}
1066
1085
  open={Boolean(popupCell)}
1067
1086
  onClose={onPopupClose}
1068
1087
  cellRect={popupCell?.cellRect}
@@ -1118,13 +1137,16 @@ function EntitiesCount({
1118
1137
  collection,
1119
1138
  filter,
1120
1139
  sortBy,
1121
- onCountChange
1140
+ onCountChange,
1141
+ pathSegments
1122
1142
  }: {
1123
1143
  fullPath: string,
1124
1144
  collection: EntityCollection,
1125
1145
  filter?: FilterValues<any>,
1126
1146
  sortBy?: [string, "asc" | "desc"],
1127
1147
  onCountChange?: (count: number) => void,
1148
+ /** `fullPath` split at its real segment boundaries; forwarded to countEntities. */
1149
+ pathSegments?: string[],
1128
1150
  }) {
1129
1151
 
1130
1152
  const dataSource = useDataSource(collection);
@@ -1134,18 +1156,24 @@ function EntitiesCount({
1134
1156
 
1135
1157
  const sortByProperty = sortBy ? sortBy[0] : undefined;
1136
1158
  const currentSort = sortBy ? sortBy[1] : undefined;
1137
- const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
1159
+ // Resolved together so the two representations always describe the same chain.
1160
+ // Segments are resolved, never derived (see useEntityFetch).
1161
+ const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
1162
+ const resolvedPathSegments = useMemo(() => pathSegments
1163
+ ? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
1164
+ : undefined, [pathSegments, navigation.resolveSegmentsFrom]);
1138
1165
 
1139
1166
  useEffect(() => {
1140
1167
  if (dataSource.countEntities)
1141
1168
  dataSource.countEntities({
1142
1169
  path: resolvedPath,
1170
+ pathSegments: resolvedPathSegments,
1143
1171
  collection,
1144
1172
  filter,
1145
1173
  orderBy: sortByProperty,
1146
1174
  order: currentSort
1147
1175
  }).then(setCount).catch(setError);
1148
- }, [fullPath, dataSource.countEntities, resolvedPath, collection, filter, sortByProperty, currentSort]);
1176
+ }, [fullPath, dataSource.countEntities, resolvedPath, resolvedPathSegments, collection, filter, sortByProperty, currentSort]);
1149
1177
 
1150
1178
  useEffect(() => {
1151
1179
  if (onCountChange && count !== undefined) {
@@ -1174,10 +1202,13 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
1174
1202
  function EntityIdHeaderWidget({
1175
1203
  collection,
1176
1204
  path,
1205
+ pathSegments,
1177
1206
  fullIdPath
1178
1207
  }: {
1179
1208
  collection: EntityCollection,
1180
1209
  path: string,
1210
+ /** `path` split at its real segment boundaries, when the caller knows them. */
1211
+ pathSegments?: string[],
1181
1212
  fullIdPath: string
1182
1213
  }) {
1183
1214
 
@@ -1217,6 +1248,7 @@ function EntityIdHeaderWidget({
1217
1248
  collection,
1218
1249
  entityId,
1219
1250
  path,
1251
+ pathSegments,
1220
1252
  fullIdPath,
1221
1253
  sideEntityController,
1222
1254
  navigation
@@ -1242,7 +1274,7 @@ function EntityIdHeaderWidget({
1242
1274
  </form>
1243
1275
  {recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
1244
1276
  {recentIds.map(id => (
1245
- <ReferencePreview reference={new EntityReference(id, path)}
1277
+ <ReferencePreview reference={new EntityReference(id, path, undefined, pathSegments)}
1246
1278
  key={id}
1247
1279
  hover={true}
1248
1280
  onClick={() => {
@@ -1252,6 +1284,7 @@ function EntityIdHeaderWidget({
1252
1284
  collection,
1253
1285
  entityId: id,
1254
1286
  path,
1287
+ pathSegments,
1255
1288
  fullIdPath,
1256
1289
  sideEntityController,
1257
1290
  navigation
@@ -22,6 +22,8 @@ import { useTranslation } from "../../hooks/useTranslation";
22
22
  export type EntityCollectionViewActionsProps<M extends Record<string, any>> = {
23
23
  collection: EntityCollection<M>;
24
24
  path: string;
25
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
26
+ pathSegments?: string[];
25
27
  relativePath: string;
26
28
  parentCollectionIds: string[];
27
29
  selectionEnabled: boolean;
@@ -40,6 +42,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
40
42
  onMultipleDeleteClick,
41
43
  selectionEnabled,
42
44
  path,
45
+ pathSegments,
43
46
  selectionController,
44
47
  tableController,
45
48
  collectionEntitiesCount,
@@ -56,7 +59,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
56
59
 
57
60
  const selectedEntities = selectionController.selectedEntities;
58
61
 
59
- const addButton = canCreateEntity(collection, authController, path, null) &&
62
+ const addButton = canCreateEntity(collection, authController, path, null, pathSegments) &&
60
63
  onNewClick && (largeLayout
61
64
  ? <Button
62
65
  id={`add_entity_${path}`}
@@ -75,7 +78,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
75
78
  <AddIcon size={"small"} />
76
79
  </Button>);
77
80
 
78
- const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null);
81
+ const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null, pathSegments);
79
82
 
80
83
  let multipleDeleteButton: React.ReactNode | undefined;
81
84
  if (selectionEnabled) {
@@ -107,6 +110,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
107
110
 
108
111
  const actionProps: CollectionActionsProps = {
109
112
  path,
113
+ pathSegments,
110
114
  relativePath,
111
115
  parentCollectionIds,
112
116
  collection,
@@ -17,6 +17,8 @@ import { useTranslation } from "../../hooks/useTranslation";
17
17
  export type EntityCollectionViewStartActionsProps<M extends Record<string, any>> = {
18
18
  collection: EntityCollection<M>;
19
19
  path: string;
20
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
21
+ pathSegments?: string[];
20
22
  relativePath: string;
21
23
  parentCollectionIds: string[];
22
24
  selectionController: SelectionController<M>;
@@ -33,6 +35,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
33
35
  relativePath,
34
36
  parentCollectionIds,
35
37
  path,
38
+ pathSegments,
36
39
  selectionController,
37
40
  tableController,
38
41
  collectionEntitiesCount,
@@ -57,6 +60,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
57
60
 
58
61
  const actionProps: CollectionActionsProps = {
59
62
  path,
63
+ pathSegments,
60
64
  relativePath,
61
65
  parentCollectionIds,
62
66
  collection,
@@ -45,6 +45,8 @@ export interface BoardDataController<M extends Record<string, any> = any, COLUMN
45
45
  export interface UseBoardDataControllerProps<M extends Record<string, any> = any> {
46
46
  /** Full path to the collection */
47
47
  fullPath: string;
48
+ /** `fullPath` split at its real segment boundaries; forwarded to the datasource. */
49
+ pathSegments?: string[];
48
50
  /** The entity collection configuration */
49
51
  collection: EntityCollection<M>;
50
52
  /** Property key used for column assignment */
@@ -67,6 +69,7 @@ export interface UseBoardDataControllerProps<M extends Record<string, any> = any
67
69
  */
68
70
  export function useBoardDataController<M extends Record<string, any> = any, COLUMN extends string = string>({
69
71
  fullPath,
72
+ pathSegments,
70
73
  collection,
71
74
  columnProperty,
72
75
  columns,
@@ -79,7 +82,12 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
79
82
  const context = useFireCMSContext();
80
83
  const dataSource = useDataSource(collection);
81
84
  const navigation = useNavigationController();
82
- const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
85
+ // Resolved together so the two representations always describe the same chain.
86
+ // Segments are resolved, never derived (see useEntityFetch).
87
+ const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
88
+ const resolvedPathSegments = useMemo(() => pathSegments
89
+ ? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
90
+ : undefined, [pathSegments, navigation.resolveSegmentsFrom]);
83
91
 
84
92
  // Stable refs for objects that shouldn't trigger re-subscriptions
85
93
  const dataSourceRef = useRef(dataSource);
@@ -95,11 +103,13 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
95
103
  const orderPropertyRef = useRef(orderProperty);
96
104
  const searchStringRef = useRef(searchString);
97
105
  const resolvedPathRef = useRef(resolvedPath);
106
+ const resolvedPathSegmentsRef = useRef(resolvedPathSegments);
98
107
  filterValuesRef.current = filterValues;
99
108
  columnPropertyRef.current = columnProperty;
100
109
  orderPropertyRef.current = orderProperty;
101
110
  searchStringRef.current = searchString;
102
111
  resolvedPathRef.current = resolvedPath;
112
+ resolvedPathSegmentsRef.current = resolvedPathSegments;
103
113
 
104
114
  // Track item count per column for pagination
105
115
  const [columnItemCounts, setColumnItemCounts] = useState<Record<string, number>>(() => {
@@ -168,6 +178,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
168
178
  const currentOrderProperty = orderPropertyRef.current;
169
179
  const currentSearchString = searchStringRef.current;
170
180
  const currentResolvedPath = resolvedPathRef.current;
181
+ const currentPathSegments = resolvedPathSegmentsRef.current;
171
182
 
172
183
  // Build filter for this column
173
184
  const columnFilter: FilterValues<string> = {
@@ -205,6 +216,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
205
216
  currentCollection.callbacks!.onFetch!({
206
217
  collection: currentCollection,
207
218
  path: currentResolvedPath,
219
+ pathSegments: currentPathSegments,
208
220
  entity,
209
221
  context: currentContext
210
222
  })
@@ -248,6 +260,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
248
260
  if (currentDataSource.listenCollection) {
249
261
  const unsubscribe = currentDataSource.listenCollection<M>({
250
262
  path: currentResolvedPath,
263
+ pathSegments: currentPathSegments,
251
264
  collection: currentCollection,
252
265
  onUpdate,
253
266
  onError,
@@ -262,6 +275,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
262
275
  } else {
263
276
  currentDataSource.fetchCollection<M>({
264
277
  path: currentResolvedPath,
278
+ pathSegments: currentPathSegments,
265
279
  collection: currentCollection,
266
280
  searchString: currentSearchString,
267
281
  filter: columnFilter,
@@ -297,6 +311,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
297
311
  const currentColumnProperty = columnPropertyRef.current;
298
312
  const currentSearchString = searchStringRef.current;
299
313
  const currentResolvedPath = resolvedPathRef.current;
314
+ const currentPathSegments = resolvedPathSegmentsRef.current;
300
315
  const currentColumns = columns;
301
316
  const currentColumnItemCounts = columnItemCounts;
302
317
 
@@ -316,6 +331,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
316
331
  };
317
332
  currentDataSource.countEntities({
318
333
  path: currentResolvedPath,
334
+ pathSegments: currentPathSegments,
319
335
  collection: currentCollection,
320
336
  filter: columnFilter,
321
337
  searchString: currentSearchString
@@ -63,7 +63,7 @@ export function EntityPreview({
63
63
 
64
64
  const navigationController = useNavigationController();
65
65
 
66
- const collection = collectionProp ?? navigationController.getCollection(entity.path);
66
+ const collection = collectionProp ?? navigationController.getCollection(entity.path, false, entity.pathSegments);
67
67
 
68
68
  if (!collection) {
69
69
  throw Error(`Couldn't find the corresponding collection view for the path: ${entity.path}`);
@@ -177,6 +177,9 @@ export function EntityPreview({
177
177
  sideEntityController.open({
178
178
  entityId: entity.id,
179
179
  path: entity.path,
180
+ // Paired with `entity.path` above; undefined if the entity was
181
+ // loaded without them.
182
+ pathSegments: entity.pathSegments,
180
183
  collection,
181
184
  updateUrl: true
182
185
  });
@@ -4,17 +4,16 @@ import { useTranslation } from "../hooks/useTranslation";
4
4
  import { ErrorIcon, Typography } from "@firecms/ui";
5
5
 
6
6
  export class ErrorBoundary extends React.Component<PropsWithChildren<Record<string, unknown>>, {
7
- hasError: boolean,
8
- error?: Error
7
+ error: Error | null
9
8
  }> {
10
9
  constructor(props: any) {
11
10
  super(props);
12
- this.state = { hasError: false };
11
+ this.state = { error: null };
13
12
  }
14
13
 
15
14
  // eslint-disable-next-line n/handle-callback-err
16
15
  static getDerivedStateFromError(error: Error) {
17
- return { hasError: true, error };
16
+ return { error };
18
17
  }
19
18
 
20
19
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
@@ -23,9 +22,8 @@ export class ErrorBoundary extends React.Component<PropsWithChildren<Record<stri
23
22
  }
24
23
 
25
24
  render() {
26
- if (this.state.hasError) {
27
- // You can render any custom fallback UI
28
- return <FallbackView message={this.state.error?.message}/>;
25
+ if (this.state.error) {
26
+ return <FallbackView message={this.state.error.message}/>;
29
27
  }
30
28
 
31
29
  return this.props.children;
@@ -35,18 +33,14 @@ export class ErrorBoundary extends React.Component<PropsWithChildren<Record<stri
35
33
  function FallbackView({ message }: { message?: string }) {
36
34
  const { t } = useTranslation();
37
35
  return (
38
- <div className="h-full w-full bg-slate-100 dark:bg-surface-900 flex items-center justify-center p-4">
39
- <div
40
- className="flex flex-col items-center justify-center m-4 bg-white dark:bg-surface-800 p-8 rounded-lg shadow-sm border border-gray-200 dark:border-surface-700">
41
- <div className="flex items-center mb-4 text-red-500 dark:text-red-400">
42
- <ErrorIcon/>
43
- <div className="ml-4">{t("error")}</div>
44
- </div>
45
- <div className="flex justify-center text-gray-500 dark:text-gray-400">
46
- {/* Error message is purposely removed since it's hard to access state here, but typical ErrorBoundary fallback doesn't always show the raw message */}
47
- {t("see_console_details")}
48
- </div>
36
+ <div className="flex flex-col m-2">
37
+ <div className="flex items-center m-2">
38
+ <ErrorIcon color={"error"} size={"small"}/>
39
+ <div className="ml-4">{t("error")}</div>
49
40
  </div>
41
+ <Typography variant={"caption"}>
42
+ {message ?? t("see_console_details")}
43
+ </Typography>
50
44
  </div>
51
45
  );
52
46
  }