@firecms/core 3.0.1 → 3.1.0-canary.24c8270

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 (186) hide show
  1. package/README.md +1 -1
  2. package/dist/components/AIIcon.d.ts +16 -0
  3. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +7 -1
  4. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +1 -1
  5. package/dist/components/EntityCollectionTable/EntityCollectionTableProps.d.ts +14 -0
  6. package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +6 -0
  7. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +5 -4
  8. package/dist/components/EntityCollectionTable/internal/EntityTableCell.d.ts +6 -0
  9. package/dist/components/EntityCollectionTable/internal/popup_field/useDraggable.d.ts +2 -2
  10. package/dist/components/EntityCollectionView/Board.d.ts +2 -0
  11. package/dist/components/EntityCollectionView/BoardColumn.d.ts +42 -0
  12. package/dist/components/EntityCollectionView/BoardColumnTitle.d.ts +9 -0
  13. package/dist/components/EntityCollectionView/BoardSortableList.d.ts +14 -0
  14. package/dist/components/EntityCollectionView/EntityBoardCard.d.ts +26 -0
  15. package/dist/components/EntityCollectionView/EntityCard.d.ts +19 -0
  16. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +20 -0
  17. package/dist/components/EntityCollectionView/EntityCollectionCardView.d.ts +31 -0
  18. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +2 -2
  19. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +7 -3
  20. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +14 -0
  21. package/dist/components/EntityCollectionView/ViewModeToggle.d.ts +44 -0
  22. package/dist/components/EntityCollectionView/board_types.d.ts +105 -0
  23. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +60 -0
  24. package/dist/components/ErrorBoundary.d.ts +1 -1
  25. package/dist/components/SelectableTable/SelectableTable.d.ts +5 -1
  26. package/dist/components/SelectableTable/filters/DateTimeFilterField.d.ts +2 -1
  27. package/dist/components/VirtualTable/VirtualTableCell.d.ts +6 -0
  28. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +3 -1
  29. package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  30. package/dist/components/VirtualTable/VirtualTableProps.d.ts +11 -0
  31. package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
  32. package/dist/components/VirtualTable/types.d.ts +2 -0
  33. package/dist/components/index.d.ts +3 -0
  34. package/dist/contexts/index.d.ts +10 -0
  35. package/dist/core/DrawerNavigationGroup.d.ts +45 -0
  36. package/dist/core/index.d.ts +1 -0
  37. package/dist/form/components/ErrorFocus.d.ts +1 -1
  38. package/dist/form/validation.d.ts +3 -2
  39. package/dist/hooks/useBreadcrumbsController.d.ts +16 -0
  40. package/dist/hooks/useCollapsedGroups.d.ts +4 -1
  41. package/dist/index.es.js +5316 -1592
  42. package/dist/index.es.js.map +1 -1
  43. package/dist/index.umd.js +5309 -1586
  44. package/dist/index.umd.js.map +1 -1
  45. package/dist/internal/useRestoreScroll.d.ts +1 -1
  46. package/dist/preview/PropertyPreviewProps.d.ts +5 -0
  47. package/dist/preview/components/DatePreview.d.ts +13 -3
  48. package/dist/preview/components/ImagePreview.d.ts +5 -1
  49. package/dist/preview/components/StorageThumbnail.d.ts +2 -1
  50. package/dist/preview/components/UrlComponentPreview.d.ts +2 -1
  51. package/dist/preview/property_previews/ArrayOfStorageComponentsPreview.d.ts +1 -1
  52. package/dist/preview/property_previews/ArrayOfStringsPreview.d.ts +1 -1
  53. package/dist/preview/property_previews/SkeletonPropertyComponent.d.ts +1 -1
  54. package/dist/types/analytics.d.ts +1 -1
  55. package/dist/types/collections.d.ts +50 -2
  56. package/dist/types/datasource.d.ts +0 -1
  57. package/dist/types/plugins.d.ts +62 -1
  58. package/dist/types/properties.d.ts +259 -4
  59. package/dist/util/__tests__/conditions.test.d.ts +1 -0
  60. package/dist/util/__tests__/objects.test.d.ts +1 -0
  61. package/dist/util/conditions.d.ts +26 -0
  62. package/dist/util/entities.d.ts +2 -3
  63. package/dist/util/index.d.ts +2 -1
  64. package/dist/util/property_utils.d.ts +2 -1
  65. package/dist/util/resolutions.d.ts +3 -3
  66. package/package.json +14 -11
  67. package/src/app/Scaffold.tsx +14 -15
  68. package/src/components/AIIcon.tsx +39 -0
  69. package/src/components/ArrayContainer.tsx +1 -4
  70. package/src/components/ClearFilterSortButton.tsx +19 -16
  71. package/src/components/ConfirmationDialog.tsx +0 -2
  72. package/src/components/DeleteEntityDialog.tsx +2 -4
  73. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +74 -41
  74. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +130 -79
  75. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +121 -104
  76. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +132 -103
  77. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +20 -42
  78. package/src/components/EntityCollectionTable/internal/EntityTableCell.tsx +90 -49
  79. package/src/components/EntityCollectionTable/internal/EntityTableCellActions.tsx +1 -1
  80. package/src/components/EntityCollectionTable/internal/popup_field/useDraggable.tsx +11 -11
  81. package/src/components/EntityCollectionView/Board.tsx +324 -0
  82. package/src/components/EntityCollectionView/BoardColumn.tsx +158 -0
  83. package/src/components/EntityCollectionView/BoardColumnTitle.tsx +45 -0
  84. package/src/components/EntityCollectionView/BoardSortableList.tsx +172 -0
  85. package/src/components/EntityCollectionView/EntityBoardCard.tsx +212 -0
  86. package/src/components/EntityCollectionView/EntityCard.tsx +235 -0
  87. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +733 -0
  88. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +244 -0
  89. package/src/components/EntityCollectionView/EntityCollectionView.tsx +519 -203
  90. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +31 -19
  91. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +84 -15
  92. package/src/components/EntityCollectionView/FiltersDialog.tsx +249 -0
  93. package/src/components/EntityCollectionView/ViewModeToggle.tsx +199 -0
  94. package/src/components/EntityCollectionView/board_types.ts +113 -0
  95. package/src/components/EntityCollectionView/useBoardDataController.tsx +490 -0
  96. package/src/components/ErrorTooltip.tsx +2 -1
  97. package/src/components/HomePage/DefaultHomePage.tsx +47 -10
  98. package/src/components/HomePage/HomePageDnD.tsx +56 -41
  99. package/src/components/HomePage/NavigationCard.tsx +20 -18
  100. package/src/components/HomePage/NavigationGroup.tsx +17 -16
  101. package/src/components/HomePage/RenameGroupDialog.tsx +0 -2
  102. package/src/components/HomePage/SmallNavigationCard.tsx +10 -9
  103. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -10
  104. package/src/components/ReferenceWidget.tsx +2 -4
  105. package/src/components/SelectableTable/SelectableTable.tsx +75 -67
  106. package/src/components/SelectableTable/filters/BooleanFilterField.tsx +7 -6
  107. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +39 -40
  108. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +38 -38
  109. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +49 -58
  110. package/src/components/UnsavedChangesDialog.tsx +0 -2
  111. package/src/components/UserDisplay.tsx +4 -4
  112. package/src/components/VirtualTable/VirtualTable.tsx +272 -118
  113. package/src/components/VirtualTable/VirtualTableCell.tsx +18 -2
  114. package/src/components/VirtualTable/VirtualTableHeader.tsx +59 -50
  115. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +158 -42
  116. package/src/components/VirtualTable/VirtualTableProps.tsx +14 -1
  117. package/src/components/VirtualTable/VirtualTableRow.tsx +1 -1
  118. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +3 -0
  119. package/src/components/VirtualTable/fields/VirtualTableSelect.tsx +19 -6
  120. package/src/components/VirtualTable/types.tsx +2 -0
  121. package/src/components/common/useColumnsIds.tsx +95 -3
  122. package/src/components/common/useDataSourceTableController.tsx +21 -4
  123. package/src/components/index.tsx +4 -0
  124. package/src/contexts/BreacrumbsContext.tsx +15 -8
  125. package/src/contexts/index.ts +10 -0
  126. package/src/core/DefaultAppBar.tsx +40 -27
  127. package/src/core/DefaultDrawer.tsx +42 -56
  128. package/src/core/DrawerNavigationGroup.tsx +118 -0
  129. package/src/core/DrawerNavigationItem.tsx +4 -3
  130. package/src/core/EntityEditView.tsx +41 -43
  131. package/src/core/EntitySidePanel.tsx +28 -26
  132. package/src/core/SideDialogs.tsx +4 -2
  133. package/src/core/field_configs.tsx +14 -9
  134. package/src/core/index.tsx +1 -0
  135. package/src/form/EntityForm.tsx +69 -60
  136. package/src/form/PropertyFieldBinding.tsx +61 -46
  137. package/src/form/components/ErrorFocus.tsx +3 -3
  138. package/src/form/components/StorageItemPreview.tsx +2 -1
  139. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +0 -1
  140. package/src/form/field_bindings/DateTimeFieldBinding.tsx +17 -16
  141. package/src/form/field_bindings/KeyValueFieldBinding.tsx +0 -1
  142. package/src/form/field_bindings/MapFieldBinding.tsx +69 -67
  143. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +22 -18
  144. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +83 -83
  145. package/src/form/field_bindings/TextFieldBinding.tsx +71 -35
  146. package/src/form/validation.ts +245 -160
  147. package/src/hooks/useBreadcrumbsController.tsx +18 -0
  148. package/src/hooks/useBuildNavigationController.tsx +71 -28
  149. package/src/hooks/useCollapsedGroups.ts +12 -4
  150. package/src/hooks/useValidateAuthenticator.tsx +1 -1
  151. package/src/internal/useBuildDataSource.ts +68 -34
  152. package/src/internal/useBuildSideDialogsController.tsx +11 -8
  153. package/src/internal/useBuildSideEntityController.tsx +24 -24
  154. package/src/internal/useRestoreScroll.tsx +26 -14
  155. package/src/preview/PropertyPreview.tsx +41 -32
  156. package/src/preview/PropertyPreviewProps.tsx +6 -0
  157. package/src/preview/components/DatePreview.tsx +72 -4
  158. package/src/preview/components/EmptyValue.tsx +1 -1
  159. package/src/preview/components/ImagePreview.tsx +37 -21
  160. package/src/preview/components/StorageThumbnail.tsx +16 -12
  161. package/src/preview/components/UrlComponentPreview.tsx +28 -25
  162. package/src/preview/property_previews/ArrayOfStorageComponentsPreview.tsx +9 -7
  163. package/src/preview/property_previews/ArrayOfStringsPreview.tsx +11 -9
  164. package/src/preview/property_previews/ArrayPropertyPreview.tsx +26 -24
  165. package/src/preview/property_previews/SkeletonPropertyComponent.tsx +61 -56
  166. package/src/routes/CustomCMSRoute.tsx +1 -0
  167. package/src/routes/FireCMSRoute.tsx +26 -13
  168. package/src/types/analytics.ts +10 -0
  169. package/src/types/collections.ts +57 -3
  170. package/src/types/datasource.ts +54 -56
  171. package/src/types/plugins.tsx +69 -1
  172. package/src/types/properties.ts +347 -27
  173. package/src/util/__tests__/conditions.test.ts +506 -0
  174. package/src/util/__tests__/objects.test.ts +196 -0
  175. package/src/util/callbacks.ts +6 -3
  176. package/src/util/collections.ts +51 -6
  177. package/src/util/conditions.ts +339 -0
  178. package/src/util/entities.ts +29 -30
  179. package/src/util/entity_cache.ts +2 -1
  180. package/src/util/index.ts +2 -1
  181. package/src/util/join_collections.ts +10 -8
  182. package/src/util/objects.ts +31 -13
  183. package/src/util/{references.ts → previews.ts} +16 -2
  184. package/src/util/property_utils.tsx +37 -11
  185. package/src/util/resolutions.ts +62 -58
  186. /package/dist/util/{references.d.ts → previews.d.ts} +0 -0
@@ -122,10 +122,10 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
122
122
 
123
123
  const navigate = useNavigate();
124
124
 
125
- const collectionsRef = useRef<EntityCollection[] | undefined>();
126
- const viewsRef = useRef<CMSView[] | undefined>();
127
- const adminViewsRef = useRef<CMSView[] | undefined>();
128
- const navigationEntriesOrderRef = useRef<string[] | undefined>();
125
+ const collectionsRef = useRef<EntityCollection[] | undefined>(undefined);
126
+ const viewsRef = useRef<CMSView[] | undefined>(undefined);
127
+ const adminViewsRef = useRef<CMSView[] | undefined>(undefined);
128
+ const navigationEntriesOrderRef = useRef<string[] | undefined>(undefined);
129
129
 
130
130
  const [initialised, setInitialised] = useState<boolean>(false);
131
131
 
@@ -140,8 +140,12 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
140
140
 
141
141
  const fullCollectionPath = cleanBasePath ? `/${cleanBasePath}/${cleanBaseCollectionPath}` : `/${cleanBaseCollectionPath}`;
142
142
 
143
- const buildCMSUrlPath = useCallback((path: string): string => cleanBasePath ? `/${cleanBasePath}/${encodePath(path)}` : `/${encodePath(path)}`,
144
- [cleanBasePath]);
143
+
144
+ const buildCMSUrlPath = useCallback((path: string): string => {
145
+ // Strip trailing /* wildcard from paths (used for nested routes in React Router)
146
+ const cleanPath = path.replace(/\/\*$/, "");
147
+ return cleanBasePath ? `/${cleanBasePath}/${encodePath(cleanPath)}` : `/${encodePath(cleanPath)}`;
148
+ }, [cleanBasePath]);
145
149
 
146
150
  const buildUrlCollectionPath = useCallback((path: string): string => `${removeInitialAndTrailingSlashes(baseCollectionPath)}/${encodePath(path)}`,
147
151
  [baseCollectionPath]);
@@ -331,10 +335,10 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
331
335
  try {
332
336
 
333
337
  const [resolvedCollections = [], resolvedViews, resolvedAdminViews = []] = await Promise.all([
334
- resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate, plugins),
335
- resolveCMSViews(viewsProp, authController, dataSourceDelegate),
336
- resolveCMSViews(adminViewsProp, authController, dataSourceDelegate)
337
- ]
338
+ resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate, plugins),
339
+ resolveCMSViews(viewsProp, authController, dataSourceDelegate, plugins),
340
+ resolveCMSViews(adminViewsProp, authController, dataSourceDelegate)
341
+ ]
338
342
  );
339
343
 
340
344
  const computedTopLevelNav = computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder, undefined, onNavigationEntriesOrderUpdate);
@@ -483,8 +487,10 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
483
487
 
484
488
  const urlPathToDataPath = useCallback((path: string): string => {
485
489
  const decodedPath = decodeURIComponent(path);
486
- if (decodedPath.startsWith(fullCollectionPath))
487
- return decodedPath.replace(fullCollectionPath, "");
490
+ const withoutHash = decodedPath.split("#")[0];
491
+ const cleanPath = withoutHash.split("?")[0];
492
+ if (cleanPath.startsWith(fullCollectionPath))
493
+ return cleanPath.replace(fullCollectionPath, "");
488
494
  throw Error("Expected path starting with " + fullCollectionPath);
489
495
  }, [fullCollectionPath]);
490
496
 
@@ -561,9 +567,27 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
561
567
  }
562
568
 
563
569
  function encodePath(input: string) {
564
- return encodeURIComponent(removeInitialAndTrailingSlashes(input))
565
- .replaceAll("%2F", "/")
566
- .replaceAll("%23", "#");
570
+ const cleanInput = removeInitialAndTrailingSlashes(input);
571
+ const [pathPart, rest] = cleanInput.split("?", 2);
572
+
573
+ let encodedPath = encodeURIComponent(pathPart).replaceAll("%2F", "/");
574
+ let result = encodedPath;
575
+
576
+ if (rest !== undefined) {
577
+ const [searchPart, hashPart] = rest.split("#", 2);
578
+ result += `?${searchPart}`;
579
+ if (hashPart !== undefined) {
580
+ result += `#${hashPart}`;
581
+ }
582
+ } else {
583
+ const [pathOnly, hashOnly] = cleanInput.split("#", 2);
584
+ if (hashOnly !== undefined) {
585
+ encodedPath = encodeURIComponent(pathOnly).replaceAll("%2F", "/");
586
+ result = `${encodedPath}#${hashOnly}`;
587
+ }
588
+ }
589
+
590
+ return result;
567
591
  }
568
592
 
569
593
  function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[], authController: AuthController<User>): EntityCollection[] {
@@ -597,10 +621,10 @@ function applyPluginModifyCollection(resolvedCollections: EntityCollection[], mo
597
621
  }
598
622
 
599
623
  async function resolveCollections(collections: undefined | EntityCollection[] | EntityCollectionsBuilder<any>,
600
- collectionPermissions: PermissionsBuilder | undefined,
601
- authController: AuthController,
602
- dataSource: DataSourceDelegate,
603
- plugins: FireCMSPlugin[] | undefined): Promise<EntityCollection[]> {
624
+ collectionPermissions: PermissionsBuilder | undefined,
625
+ authController: AuthController,
626
+ dataSource: DataSourceDelegate,
627
+ plugins: FireCMSPlugin[] | undefined): Promise<EntityCollection[]> {
604
628
  let resolvedCollections: EntityCollection[] = [];
605
629
  if (typeof collections === "function") {
606
630
  resolvedCollections = await collections({
@@ -629,7 +653,12 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
629
653
  return resolvedCollections;
630
654
  }
631
655
 
632
- async function resolveCMSViews(baseViews: CMSView[] | CMSViewsBuilder | undefined, authController: AuthController, dataSource: DataSourceDelegate) {
656
+ async function resolveCMSViews(
657
+ baseViews: CMSView[] | CMSViewsBuilder | undefined,
658
+ authController: AuthController,
659
+ dataSource: DataSourceDelegate,
660
+ plugins?: FireCMSPlugin[]
661
+ ) {
633
662
  let resolvedViews: CMSView[] = [];
634
663
  if (typeof baseViews === "function") {
635
664
  resolvedViews = await baseViews({
@@ -640,6 +669,16 @@ async function resolveCMSViews(baseViews: CMSView[] | CMSViewsBuilder | undefine
640
669
  } else if (Array.isArray(baseViews)) {
641
670
  resolvedViews = baseViews;
642
671
  }
672
+
673
+ // Inject views from plugins
674
+ if (plugins) {
675
+ for (const plugin of plugins) {
676
+ if (plugin.views && plugin.views.length > 0) {
677
+ resolvedViews = [...resolvedViews, ...plugin.views];
678
+ }
679
+ }
680
+ }
681
+
643
682
  return resolvedViews;
644
683
  }
645
684
 
@@ -688,8 +727,8 @@ function useCustomBlocker(): NavigationBlocker {
688
727
  let blocker: any;
689
728
  try {
690
729
  blocker = useBlocker(({
691
- nextLocation
692
- }) => {
730
+ nextLocation
731
+ }) => {
693
732
  const allBasePaths = Object.values(blockListeners).map(b => b.basePath).filter(Boolean) as string[];
694
733
  if (allBasePaths && allBasePaths.some(path => nextLocation.pathname.startsWith(path)))
695
734
  return false;
@@ -729,11 +768,11 @@ function useCustomBlocker(): NavigationBlocker {
729
768
  }
730
769
 
731
770
  function computeNavigationGroups({
732
- navigationGroupMappings,
733
- collections,
734
- views,
735
- plugins
736
- }: {
771
+ navigationGroupMappings,
772
+ collections,
773
+ views,
774
+ plugins
775
+ }: {
737
776
  navigationGroupMappings?: NavigationGroupMapping[],
738
777
  collections?: EntityCollection[],
739
778
  views?: CMSView[],
@@ -743,6 +782,7 @@ function computeNavigationGroups({
743
782
  let result = navigationGroupMappings;
744
783
 
745
784
  // Merge plugin navigation entries
785
+ // IMPORTANT: Deep clone the groups to avoid mutating the original input
746
786
  result = plugins ? plugins?.reduce((acc, plugin) => {
747
787
  if (plugin.homePage?.navigationEntries) {
748
788
  plugin.homePage.navigationEntries.forEach((entry) => {
@@ -763,7 +803,10 @@ function computeNavigationGroups({
763
803
 
764
804
  }
765
805
  return acc;
766
- }, [...(result ?? [])] as NavigationGroupMapping[]) : result;
806
+ }, (result ?? []).map(g => ({
807
+ name: g.name,
808
+ entries: [...g.entries]
809
+ }))) : result;
767
810
 
768
811
  // Track all entries that are already assigned to groups
769
812
  const assignedEntries = new Set<string>();
@@ -1,15 +1,22 @@
1
1
  import { useCallback, useEffect, useState } from "react";
2
2
 
3
+ const STORAGE_KEY_PREFIX = "firecms-collapsed-groups";
4
+
3
5
  /**
4
6
  * Custom hook for managing collapsed/expanded state of navigation groups
5
7
  * with localStorage persistence. Automatically cleans up stale group entries
6
8
  * when groups are removed from the navigation.
9
+ *
10
+ * @param groupNames - Array of group names to track
11
+ * @param namespace - Namespace for localStorage key (e.g., "home", "drawer") to allow independent state
7
12
  */
8
- export function useCollapsedGroups(groupNames: string[]) {
13
+ export function useCollapsedGroups(groupNames: string[], namespace: string = "default") {
14
+ const storageKey = `${STORAGE_KEY_PREFIX}-${namespace}`;
15
+
9
16
  // Load collapsed groups from localStorage on mount
10
17
  const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>(() => {
11
18
  try {
12
- const stored = localStorage.getItem('firecms-collapsed-groups');
19
+ const stored = localStorage.getItem(storageKey);
13
20
  return stored ? JSON.parse(stored) : {};
14
21
  } catch {
15
22
  return {};
@@ -19,11 +26,11 @@ export function useCollapsedGroups(groupNames: string[]) {
19
26
  // Save to localStorage whenever collapsedGroups changes
20
27
  useEffect(() => {
21
28
  try {
22
- localStorage.setItem('firecms-collapsed-groups', JSON.stringify(collapsedGroups));
29
+ localStorage.setItem(storageKey, JSON.stringify(collapsedGroups));
23
30
  } catch {
24
31
  // Silently fail if localStorage is not available
25
32
  }
26
- }, [collapsedGroups]);
33
+ }, [collapsedGroups, storageKey]);
27
34
 
28
35
  // Clean up collapsed groups state when groups change - remove entries for groups that no longer exist
29
36
  useEffect(() => {
@@ -62,3 +69,4 @@ export function useCollapsedGroups(groupNames: string[]) {
62
69
  toggleGroupCollapsed
63
70
  };
64
71
  }
72
+
@@ -51,7 +51,7 @@ export function useValidateAuthenticator<USER extends User = any>
51
51
  * We use this ref to check the authentication only if the user has
52
52
  * changed.
53
53
  */
54
- const checkedUserRef = useRef<User | undefined>();
54
+ const checkedUserRef = useRef<User | undefined>(undefined);
55
55
 
56
56
  const checkAuthentication = useCallback(async () => {
57
57
 
@@ -26,11 +26,11 @@ import { resolveCollection, updateDateAutoValues } from "../util";
26
26
  * @group Firebase
27
27
  */
28
28
  export function useBuildDataSource({
29
- delegate,
30
- propertyConfigs,
31
- navigationController,
32
- authController
33
- }: {
29
+ delegate,
30
+ propertyConfigs,
31
+ navigationController,
32
+ authController
33
+ }: {
34
34
  delegate: DataSourceDelegate,
35
35
  propertyConfigs?: Record<string, PropertyConfig>;
36
36
  navigationController: NavigationController;
@@ -54,15 +54,15 @@ export function useBuildDataSource({
54
54
  * @group Firestore
55
55
  */
56
56
  fetchCollection: useCallback(<M extends Record<string, any>>({
57
- path,
58
- collection,
59
- filter,
60
- limit,
61
- startAfter,
62
- searchString,
63
- orderBy,
64
- order,
65
- }: FetchCollectionProps<M>
57
+ path,
58
+ collection,
59
+ filter,
60
+ limit,
61
+ startAfter,
62
+ searchString,
63
+ orderBy,
64
+ order,
65
+ }: FetchCollectionProps<M>
66
66
  ): Promise<Entity<M>[]> => {
67
67
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
68
68
  return usedDelegate.fetchCollection<M>({
@@ -138,10 +138,10 @@ export function useBuildDataSource({
138
138
  * @group Firestore
139
139
  */
140
140
  fetchEntity: useCallback(<M extends Record<string, any>>({
141
- path,
142
- entityId,
143
- collection
144
- }: FetchEntityProps<M>
141
+ path,
142
+ entityId,
143
+ collection
144
+ }: FetchEntityProps<M>
145
145
  ): Promise<Entity<M> | undefined> => {
146
146
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
147
147
  return usedDelegate.fetchEntity({
@@ -195,7 +195,7 @@ export function useBuildDataSource({
195
195
  * @param status
196
196
  * @group Firestore
197
197
  */
198
- saveEntity: useCallback(<M extends Record<string, any>>(
198
+ saveEntity: useCallback(async <M extends Record<string, any>>(
199
199
  {
200
200
  path,
201
201
  entityId,
@@ -229,22 +229,56 @@ export function useBuildDataSource({
229
229
  inputValues: delegateValues,
230
230
  properties,
231
231
  status,
232
- timestampNowValue: usedDelegate.currentTime?.() ?? new Date(),
233
- setDateToMidnight: usedDelegate.setDateToMidnight
232
+ timestampNowValue: usedDelegate.currentTime?.() ?? new Date()
234
233
  })
235
234
  : delegateValues;
236
235
 
236
+ // Auto-assign order property value for new/copy entities
237
+ let finalValues = updatedValues;
238
+ const orderProperty = collection?.orderProperty;
239
+ if (orderProperty && (status === "new" || status === "copy")) {
240
+ const orderProp = properties?.[orderProperty as keyof M];
241
+ if (orderProp) {
242
+ const currentValue = updatedValues[orderProperty as keyof M];
243
+ if (currentValue === undefined || currentValue === null) {
244
+ try {
245
+ const entities = await usedDelegate.fetchCollection({
246
+ path,
247
+ orderBy: orderProperty,
248
+ order: "asc",
249
+ limit: 1,
250
+ collection
251
+ });
252
+ const minOrder = entities.length > 0
253
+ ? entities[0].values?.[orderProperty] ?? null
254
+ : null;
255
+ finalValues = {
256
+ ...updatedValues,
257
+ [orderProperty]: minOrder !== null ? minOrder - 1 : 0
258
+ } as EntityValues<M>;
259
+ } catch (e) {
260
+ console.error("Failed to fetch min order value:", e);
261
+ // Fallback to 0 if query fails
262
+ finalValues = {
263
+ ...updatedValues,
264
+ [orderProperty]: 0
265
+ } as EntityValues<M>;
266
+ }
267
+ }
268
+ }
269
+ }
270
+
237
271
  return usedDelegate.saveEntity({
238
272
  path,
239
273
  collection,
240
274
  entityId,
241
- values: updatedValues,
275
+ values: finalValues,
242
276
  status
243
277
  }).then((res) => {
244
278
  return {
245
279
  id: res.id,
246
280
  path: res.path,
247
- values: usedDelegate.delegateToCMSModel(updatedValues)
281
+ values: usedDelegate.delegateToCMSModel(finalValues)
248
282
  } as Entity<M>;
249
283
  });
250
284
  }, [delegate.saveEntity, navigationController.getCollection]),
@@ -295,12 +329,12 @@ export function useBuildDataSource({
295
329
  }, [delegate.generateEntityId]),
296
330
 
297
331
  countEntities: delegate.countEntities ? async ({
298
- path,
299
- collection,
300
- filter,
301
- order,
302
- orderBy
303
- }: {
332
+ path,
333
+ collection,
334
+ filter,
335
+ order,
336
+ orderBy
337
+ }: {
304
338
  path: string,
305
339
  collection: EntityCollection<any>,
306
340
  filter?: FilterValues<Extract<keyof any, string>>,
@@ -318,11 +352,11 @@ export function useBuildDataSource({
318
352
  } : undefined,
319
353
 
320
354
  isFilterCombinationValid: useCallback(({
321
- path,
322
- databaseId,
323
- filterValues,
324
- sortBy
325
- }: {
355
+ path,
356
+ databaseId,
357
+ filterValues,
358
+ sortBy
359
+ }: {
326
360
  path: string,
327
361
  databaseId?: string,
328
362
  filterValues: FilterValues<any>,
@@ -32,11 +32,12 @@ export function useBuildSideDialogsController(): SideDialogsController {
32
32
 
33
33
  const close = useCallback(() => {
34
34
 
35
- if (sidePanels.length === 0)
35
+ const currentPanels = sidePanelsRef.current;
36
+ if (currentPanels.length === 0)
36
37
  return;
37
38
 
38
- const lastSidePanel = sidePanels[sidePanels.length - 1];
39
- const updatedPanels = [...sidePanels.slice(0, -1)];
39
+ const lastSidePanel = currentPanels[currentPanels.length - 1];
40
+ const updatedPanels = [...currentPanels.slice(0, -1)];
40
41
  updateSidePanels(updatedPanels);
41
42
 
42
43
  if (routesCount.current > 0) {
@@ -56,7 +57,7 @@ export function useBuildSideDialogsController(): SideDialogsController {
56
57
  }
57
58
  );
58
59
  }
59
- }, [sidePanels, navigate, location]);
60
+ }, [navigate, location]);
60
61
 
61
62
  const open = useCallback((panelProps: SideDialogPanelProps | SideDialogPanelProps[]) => {
62
63
 
@@ -69,7 +70,8 @@ export function useBuildSideDialogsController(): SideDialogsController {
69
70
 
70
71
  const baseLocation = (location.state as any)?.base_location ?? location;
71
72
 
72
- const updatedPanels = [...sidePanels, ...newPanels];
73
+ const currentPanels = sidePanelsRef.current;
74
+ const updatedPanels = [...currentPanels, ...newPanels];
73
75
  updateSidePanels(updatedPanels);
74
76
 
75
77
  newPanels.forEach((panel) => {
@@ -86,7 +88,7 @@ export function useBuildSideDialogsController(): SideDialogsController {
86
88
  }
87
89
  });
88
90
 
89
- }, [location, navigate, sidePanels]);
91
+ }, [location, navigate]);
90
92
 
91
93
  const replace = useCallback((panelProps: SideDialogPanelProps | SideDialogPanelProps[]) => {
92
94
 
@@ -97,7 +99,8 @@ export function useBuildSideDialogsController(): SideDialogsController {
97
99
 
98
100
  const baseLocation = (location.state as any)?.base_location ?? location;
99
101
 
100
- const updatedPanels = [...sidePanels.slice(0, -newPanels.length), ...newPanels];
102
+ const currentPanels = sidePanelsRef.current;
103
+ const updatedPanels = [...currentPanels.slice(0, -newPanels.length), ...newPanels];
101
104
  updateSidePanels(updatedPanels);
102
105
 
103
106
  newPanels.forEach((panel) => {
@@ -115,7 +118,7 @@ export function useBuildSideDialogsController(): SideDialogsController {
115
118
  }
116
119
  });
117
120
 
118
- }, [location, navigate, sidePanels]);
121
+ }, [location, navigate]);
119
122
 
120
123
  return {
121
124
  sidePanels,
@@ -20,7 +20,6 @@ import {
20
20
  } from "../util";
21
21
  import { ADDITIONAL_TAB_WIDTH, CONTAINER_FULL_WIDTH, FORM_CONTAINER_WIDTH } from "./common";
22
22
  import { useCustomizationController, useLargeLayout } from "../hooks";
23
- import { EntitySidePanel } from "../core/EntitySidePanel";
24
23
  import { JSON_TAB_VALUE } from "../core/EntityEditView";
25
24
 
26
25
  const NEW_URL_HASH = "new_side";
@@ -100,8 +99,8 @@ function getNestedPropertiesDepth(property: ResolvedProperty, accumulator: numbe
100
99
  }
101
100
 
102
101
  export const useBuildSideEntityController = (navigation: NavigationController,
103
- sideDialogsController: SideDialogsController,
104
- authController: AuthController
102
+ sideDialogsController: SideDialogsController,
103
+ authController: AuthController
105
104
  ): SideEntityController => {
106
105
 
107
106
  const location = useLocation();
@@ -122,9 +121,9 @@ export const useBuildSideEntityController = (navigation: NavigationController,
122
121
  for (let i = 0; i < panelsFromUrl.length; i++) {
123
122
  const props = panelsFromUrl[i];
124
123
  if (i === 0)
125
- sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController));
124
+ sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search));
126
125
  else
127
- sideDialogsController.open(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController))
126
+ sideDialogsController.open(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search))
128
127
  }
129
128
  }
130
129
  initialised.current = true;
@@ -144,7 +143,7 @@ export const useBuildSideEntityController = (navigation: NavigationController,
144
143
  return;
145
144
  }
146
145
  const lastPanel = panelsFromUrl[panelsFromUrl.length - 1];
147
- const panelProps = propsToSidePanel(lastPanel, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController);
146
+ const panelProps = propsToSidePanel(lastPanel, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search);
148
147
  const lastCurrentPanel = currentPanelKeys.length > 0 ? currentPanelKeys[currentPanelKeys.length - 1] : undefined;
149
148
  if (!lastCurrentPanel || lastCurrentPanel !== panelProps.key) {
150
149
  sideDialogsController.replace(panelProps);
@@ -157,7 +156,7 @@ export const useBuildSideEntityController = (navigation: NavigationController,
157
156
  useEffect(() => {
158
157
  const updatedSidePanels = sideDialogsController.sidePanels.map(sidePanelProps => {
159
158
  if (sidePanelProps.additional) {
160
- return propsToSidePanel(sidePanelProps.additional, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController);
159
+ return propsToSidePanel(sidePanelProps.additional, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search);
161
160
  }
162
161
  return sidePanelProps;
163
162
  });
@@ -184,17 +183,18 @@ export const useBuildSideEntityController = (navigation: NavigationController,
184
183
 
185
184
  sideDialogsController.open(
186
185
  propsToSidePanel({
187
- selectedTab: defaultSelectedView,
188
- ...props
189
- },
186
+ selectedTab: defaultSelectedView,
187
+ ...props
188
+ },
190
189
  navigation.buildUrlCollectionPath,
191
190
  navigation.resolveIdsFrom,
192
191
  smallLayout,
193
192
  customizationController,
194
- authController
193
+ authController,
194
+ location.search
195
195
  ));
196
196
 
197
- }, [sideDialogsController, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, authController.user]);
197
+ }, [sideDialogsController, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, authController.user, location.search]);
198
198
 
199
199
  const replace = useCallback((props: EntitySidePanelProps<any>) => {
200
200
 
@@ -202,9 +202,9 @@ export const useBuildSideEntityController = (navigation: NavigationController,
202
202
  throw Error("If you want to copy an entity you need to provide an entityId");
203
203
  }
204
204
 
205
- sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController));
205
+ sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search));
206
206
 
207
- }, [navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, sideDialogsController, smallLayout, authController.user]);
207
+ }, [navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, sideDialogsController, smallLayout, authController.user, location.search]);
208
208
 
209
209
  return {
210
210
  close,
@@ -215,7 +215,6 @@ export const useBuildSideEntityController = (navigation: NavigationController,
215
215
 
216
216
  export function buildSidePanelsFromUrl(path: string, collections: EntityCollection[], newFlag: boolean): EntitySidePanelProps<any>[] {
217
217
 
218
-
219
218
  const navigationViewsForPath: NavigationViewInternal<any>[] = getNavigationEntriesFromPath({
220
219
  path,
221
220
  collections
@@ -267,18 +266,19 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
267
266
  }
268
267
 
269
268
  const propsToSidePanel = (props: EntitySidePanelProps,
270
- buildUrlCollectionPath: (path: string) => string,
271
- resolveIdsFrom: (pathWithAliases: string) => string,
272
- smallLayout: boolean,
273
- customizationController: CustomizationController,
274
- authController: AuthController
269
+ buildUrlCollectionPath: (path: string) => string,
270
+ resolveIdsFrom: (pathWithAliases: string) => string,
271
+ smallLayout: boolean,
272
+ customizationController: CustomizationController,
273
+ authController: AuthController,
274
+ locationSearch: string
275
275
  ): SideDialogPanelProps => {
276
276
 
277
277
  const collectionPath = removeInitialAndTrailingSlashes(props.path);
278
278
 
279
279
  const urlPath = props.entityId
280
- ? buildUrlCollectionPath(`${collectionPath}/${props.entityId}${props.selectedTab ? "/" + props.selectedTab : ""}#${SIDE_URL_HASH}`)
281
- : buildUrlCollectionPath(`${collectionPath}#${NEW_URL_HASH}`);
280
+ ? buildUrlCollectionPath(`${collectionPath}/${props.entityId}${props.selectedTab ? "/" + props.selectedTab : ""}${locationSearch}#${SIDE_URL_HASH}`)
281
+ : buildUrlCollectionPath(`${collectionPath}${locationSearch}#${NEW_URL_HASH}`);
282
282
 
283
283
  const resolvedPanelProps: EntitySidePanelProps<any> = {
284
284
  ...props,
@@ -288,12 +288,12 @@ const propsToSidePanel = (props: EntitySidePanelProps,
288
288
  const entityViewWidth = getEntityViewWidth(props, smallLayout, customizationController, authController);
289
289
  return {
290
290
  key: `${props.path}/${props.entityId}`,
291
- component: <EntitySidePanel {...resolvedPanelProps}/>,
291
+ component: undefined, // Lazy render in SideDialogs for better performance
292
292
  urlPath: urlPath,
293
293
  parentUrlPath: buildUrlCollectionPath(collectionPath),
294
294
  width: entityViewWidth,
295
295
  onClose: props.onClose,
296
- additional: props
296
+ additional: resolvedPanelProps
297
297
  };
298
298
  }
299
299
 
@@ -1,24 +1,28 @@
1
- import React, { useCallback, useEffect } from "react";
1
+ import React, { useCallback, useEffect, useRef } from "react";
2
2
  import { useLocation } from "react-router-dom";
3
3
 
4
4
  const scrollsMap: Record<string, number> = {};
5
5
 
6
6
  export function useRestoreScroll() {
7
7
 
8
- // const scrollsMap = React.useRef<Record<string, number>>({});
9
-
10
8
  const location = useLocation();
11
9
 
12
- const containerRef = React.useRef<HTMLDivElement>(null);
10
+ const containerRef = useRef<HTMLDivElement>(null);
13
11
  const [scroll, setScroll] = React.useState(0);
14
12
  const [direction, setDirection] = React.useState<"up" | "down">("down");
15
13
 
14
+ // Use ref to track previous scroll for direction calculation
15
+ // This avoids recreating handleScroll on every scroll
16
+ const prevScrollRef = useRef(0);
17
+
16
18
  const handleScroll = useCallback(() => {
17
19
  if (!containerRef.current || !location.key) return;
18
- scrollsMap[location.key] = containerRef.current.scrollTop;
19
- setScroll(containerRef.current.scrollTop);
20
- setDirection(containerRef.current.scrollTop > scroll ? "down" : "up");
21
- }, [containerRef, location.key, scroll]);
20
+ const scrollTop = containerRef.current.scrollTop;
21
+ scrollsMap[location.key] = scrollTop;
22
+ setScroll(scrollTop);
23
+ setDirection(scrollTop > prevScrollRef.current ? "down" : "up");
24
+ prevScrollRef.current = scrollTop;
25
+ }, [location.key]);
22
26
 
23
27
  useEffect(() => {
24
28
  const container = containerRef.current;
@@ -29,16 +33,24 @@ export function useRestoreScroll() {
29
33
  if (container)
30
34
  container.removeEventListener("scroll", handleScroll);
31
35
  };
32
- }, [containerRef, handleScroll, location]);
36
+ }, [handleScroll]);
33
37
 
38
+ // Defer scroll restoration to next tick to allow async content to render
39
+ // This is necessary because DefaultHomePage content loads asynchronously
34
40
  useEffect(() => {
35
- if (!containerRef.current || !scrollsMap[location.key]) return;
36
- containerRef.current.scrollTo(
37
- {
38
- top: scrollsMap[location.key],
41
+ const savedScroll = scrollsMap[location.key];
42
+ if (!containerRef.current || !savedScroll) return;
43
+
44
+ const timeoutId = setTimeout(() => {
45
+ if (!containerRef.current) return;
46
+ containerRef.current.scrollTo({
47
+ top: savedScroll,
39
48
  behavior: "auto"
40
49
  });
41
- }, [location]);
50
+ }, 0);
51
+
52
+ return () => clearTimeout(timeoutId);
53
+ }, [location.key]);
42
54
 
43
55
  return {
44
56
  containerRef,