@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
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import { EntityCollection, ResolvedEntityCollection, ResolvedProperty } from "../../types";
3
+ import { getPropertyInPath } from "../../util";
3
4
  import { getSubcollectionColumnId } from "../EntityCollectionTable/internal/common";
4
5
  import { PropertyColumnConfig } from "../EntityCollectionTable/EntityCollectionTableProps";
5
6
 
@@ -16,15 +17,45 @@ export function useColumnIds<M extends Record<string, any>>(collection: Resolved
16
17
 
17
18
  function hideAndExpandKeys<M extends Record<string, any>>(collection: ResolvedEntityCollection<M>, keys: string[]): PropertyColumnConfig[] {
18
19
 
19
- return keys.flatMap((key) => {
20
+ // First, figure out which spread map roots have individual child keys in the order
21
+ // If so, we should NOT auto-expand them - just use the explicit child keys
22
+ const rootsWithExplicitChildren = new Set<string>();
23
+ for (const key of keys) {
24
+ if (key.includes(".")) {
25
+ const rootKey = key.split(".")[0];
26
+ const rootProperty = collection.properties[rootKey];
27
+ if (rootProperty && rootProperty.dataType === "map" && rootProperty.spreadChildren && rootProperty.properties) {
28
+ rootsWithExplicitChildren.add(rootKey);
29
+ }
30
+ }
31
+ }
32
+
33
+ // Track processed keys to avoid duplicates
34
+ const processedPropertyKeys = new Set<string>();
35
+
36
+ const result = keys.flatMap((key) => {
37
+ // Skip if already processed (handles duplicates in propertiesOrder)
38
+ if (processedPropertyKeys.has(key)) return [null];
39
+
40
+ // Check if it's a top-level property
20
41
  const property = collection.properties[key];
21
42
  if (property) {
43
+ processedPropertyKeys.add(key);
22
44
  if (property.hideFromCollection)
23
45
  return [null];
24
46
  if (property.disabled && typeof property.disabled === "object" && property.disabled.hidden)
25
47
  return [null];
48
+
26
49
  if (property.dataType === "map" && property.spreadChildren && property.properties) {
27
- return getColumnKeysForProperty(property, key);
50
+ // Check if this spread map has explicit child keys in propertiesOrder
51
+ if (rootsWithExplicitChildren.has(key)) {
52
+ // DON'T auto-expand - the children are explicitly listed elsewhere
53
+ return [null];
54
+ }
55
+ // Auto-expand all children
56
+ const childConfigs = getColumnKeysForProperty(property, key);
57
+ childConfigs.forEach(c => processedPropertyKeys.add(c.key));
58
+ return childConfigs;
28
59
  }
29
60
  return [{
30
61
  key,
@@ -32,6 +63,33 @@ function hideAndExpandKeys<M extends Record<string, any>>(collection: ResolvedEn
32
63
  }];
33
64
  }
34
65
 
66
+ // Check if it's a nested key like "data.mode" (for spread map properties)
67
+ if (key.includes(".")) {
68
+ const rootKey = key.split(".")[0];
69
+ const rootProperty = collection.properties[rootKey];
70
+
71
+ if (rootProperty && rootProperty.dataType === "map" && rootProperty.properties) {
72
+ const nestedProperty = getPropertyInPath(collection.properties, key) as ResolvedProperty | undefined;
73
+ if (nestedProperty) {
74
+ processedPropertyKeys.add(key);
75
+ // Mark root as seen
76
+ processedPropertyKeys.add(rootKey);
77
+
78
+ if (nestedProperty.hideFromCollection)
79
+ return [null];
80
+ if (nestedProperty.disabled && typeof nestedProperty.disabled === "object" && nestedProperty.disabled.hidden)
81
+ return [null];
82
+
83
+ return [{
84
+ key,
85
+ disabled: Boolean(rootProperty.disabled) || Boolean(rootProperty.readOnly) ||
86
+ Boolean(nestedProperty.disabled) || Boolean(nestedProperty.readOnly)
87
+ }];
88
+ }
89
+ }
90
+ }
91
+
92
+ // Check additional fields
35
93
  const additionalField = collection.additionalFields?.find(field => field.key === key);
36
94
  if (additionalField) {
37
95
  return [{
@@ -40,6 +98,7 @@ function hideAndExpandKeys<M extends Record<string, any>>(collection: ResolvedEn
40
98
  }];
41
99
  }
42
100
 
101
+ // Check subcollections
43
102
  if (collection.subcollections) {
44
103
  const subCollection = collection.subcollections.find(subCol => getSubcollectionColumnId(subCol) === key);
45
104
  if (subCollection) {
@@ -50,6 +109,7 @@ function hideAndExpandKeys<M extends Record<string, any>>(collection: ResolvedEn
50
109
  }
51
110
  }
52
111
 
112
+ // Check collection group parent
53
113
  if (collection.collectionGroup && key === COLLECTION_GROUP_PARENT_ID) {
54
114
  return [{
55
115
  key,
@@ -59,6 +119,37 @@ function hideAndExpandKeys<M extends Record<string, any>>(collection: ResolvedEn
59
119
 
60
120
  return [null];
61
121
  }).filter(Boolean) as PropertyColumnConfig[];
122
+
123
+ // Add any missing properties that weren't in propertiesOrder
124
+ // This ensures properties NEVER disappear
125
+ for (const propKey of Object.keys(collection.properties)) {
126
+ // Skip if already processed
127
+ if (processedPropertyKeys.has(propKey)) continue;
128
+
129
+ const property = collection.properties[propKey];
130
+ if (!property) continue;
131
+ if (property.hideFromCollection) continue;
132
+ if (property.disabled && typeof property.disabled === "object" && property.disabled.hidden) continue;
133
+
134
+ if (property.dataType === "map" && property.spreadChildren && property.properties) {
135
+ // For spread maps, add all children that weren't already added
136
+ const allChildConfigs = getColumnKeysForProperty(property, propKey);
137
+ for (const childConfig of allChildConfigs) {
138
+ if (!processedPropertyKeys.has(childConfig.key)) {
139
+ result.push(childConfig);
140
+ processedPropertyKeys.add(childConfig.key);
141
+ }
142
+ }
143
+ } else {
144
+ result.push({
145
+ key: propKey,
146
+ disabled: Boolean(property.disabled) || Boolean(property.readOnly)
147
+ });
148
+ processedPropertyKeys.add(propKey);
149
+ }
150
+ }
151
+
152
+ return result;
62
153
  }
63
154
 
64
155
  function getDefaultColumnKeys<M extends Record<string, any> = any>(collection: ResolvedEntityCollection<M>, includeSubCollections: boolean) {
@@ -69,7 +160,8 @@ function getDefaultColumnKeys<M extends Record<string, any> = any>(collection: R
69
160
 
70
161
  const columnIds: string[] = [
71
162
  ...propertyKeys,
72
- ...additionalFields.map((field) => field.key)
163
+ // Filter out additional fields whose key already exists in propertyKeys to avoid duplicate column keys
164
+ ...additionalFields.filter((field) => !propertyKeys.includes(field.key)).map((field) => field.key)
73
165
  ];
74
166
 
75
167
  if (includeSubCollections) {
@@ -1,4 +1,5 @@
1
1
  import React, { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { useLocation } from "react-router-dom";
2
3
 
3
4
  import { useDataSource, useFireCMSContext, useNavigationController } from "../../hooks";
4
5
  import { useDataOrder } from "../../hooks/data/useDataOrder";
@@ -94,7 +95,7 @@ export function useDataSourceTableController<M extends Record<string, any> = any
94
95
  const [searchString, setSearchString] = React.useState<string | undefined>();
95
96
 
96
97
  const checkFilterCombination = useCallback((filterValues: FilterValues<any>,
97
- sortBy?: [string, "asc" | "desc"]) => {
98
+ sortBy?: [string, "asc" | "desc"]) => {
98
99
  if (!dataSource.isFilterCombinationValid)
99
100
  return true;
100
101
  return dataSource.isFilterCombinationValid({
@@ -106,8 +107,8 @@ export function useDataSourceTableController<M extends Record<string, any> = any
106
107
  }, []);
107
108
 
108
109
  const onScroll = ({
109
- scrollOffset
110
- }: {
110
+ scrollOffset
111
+ }: {
111
112
  scrollOffset: number
112
113
  }) => {
113
114
  if (scrollRestoration) {
@@ -128,14 +129,30 @@ export function useDataSourceTableController<M extends Record<string, any> = any
128
129
  return initialSort;
129
130
  }, [initialSort, forceFilter]);
130
131
 
132
+ const location = useLocation();
133
+
131
134
  const {
132
135
  filterValues: initialFilterUrl,
133
136
  sortBy: initialSortUrl,
134
- } = parseFilterAndSort(window.location.search);
137
+ } = parseFilterAndSort(location.search);
135
138
 
136
139
  const [filterValues, setFilterValues] = React.useState<FilterValues<Extract<keyof M, string>> | undefined>(forceFilter ?? (updateUrl ? initialFilterUrl : undefined) ?? initialFilter ?? undefined);
137
140
  const [sortBy, setSortBy] = React.useState<[Extract<keyof M, string>, "asc" | "desc"] | undefined>((updateUrl ? initialSortUrl : undefined) ?? initialSortInternal);
138
141
 
142
+ useEffect(() => {
143
+ if (updateUrl) {
144
+ const { filterValues: urlFilterValues, sortBy: urlSortBy } = parseFilterAndSort(location.search);
145
+ if (!forceFilter) {
146
+ setFilterValues(urlFilterValues as any);
147
+ }
148
+ if (urlSortBy && forceFilter && !checkFilterCombination(forceFilter, urlSortBy)) {
149
+ console.warn("URL sort is not compatible with the force filter.");
150
+ } else {
151
+ setSortBy(urlSortBy as any);
152
+ }
153
+ }
154
+ }, [location.search, updateUrl, forceFilter, checkFilterCombination]);
155
+
139
156
  useUpdateUrl(filterValues, sortBy, searchString, updateUrl);
140
157
 
141
158
  const collectionScroll = scrollRestoration?.getCollectionScroll(fullPath, filterValues);
@@ -16,6 +16,8 @@ export * from "./SelectableTable/SelectableTable";
16
16
  export * from "./SelectableTable/SelectableTableContext";
17
17
  export * from "./EntityCollectionView/EntityCollectionView";
18
18
  export * from "./EntityCollectionView/EntityCollectionViewActions";
19
+ export * from "./EntityCollectionView/EntityCollectionCardView";
20
+ export * from "./EntityCollectionView/EntityCard";
19
21
  export * from "./EntityCollectionView/useSelectionController";
20
22
 
21
23
  export * from "./PropertyConfigBadge";
@@ -38,3 +40,5 @@ export * from "./SearchIconsView";
38
40
  export * from "./FieldCaption";
39
41
 
40
42
  export * from "./EntityPreview";
43
+
44
+ export * from "./AIIcon";
@@ -1,11 +1,11 @@
1
- import React, { useState } from "react";
1
+ import React, { useCallback, useState } from "react";
2
2
  import { BreadcrumbEntry, BreadcrumbsController } from "../hooks/useBreadcrumbsController";
3
3
 
4
- const DEFAULT_BREADCRUMBS_CONTROLLER = {
4
+ const DEFAULT_BREADCRUMBS_CONTROLLER: BreadcrumbsController = {
5
5
  breadcrumbs: [],
6
- set: (props: {
7
- breadcrumbs: BreadcrumbEntry[];
8
- }) => {
6
+ set: () => {
7
+ },
8
+ updateCount: () => {
9
9
  }
10
10
  };
11
11
 
@@ -19,17 +19,24 @@ export const BreadcrumbsProvider: React.FC<BreadcrumbsProviderProps> = ({ childr
19
19
 
20
20
  const [breadcrumbs, setBreadcrumbs] = useState<BreadcrumbEntry[]>([]);
21
21
 
22
- const set = (props: {
22
+ const set = useCallback((props: {
23
23
  breadcrumbs: BreadcrumbEntry[];
24
24
  }) => {
25
25
  setBreadcrumbs(props.breadcrumbs);
26
- };
26
+ }, []);
27
+
28
+ const updateCount = useCallback((id: string, count: number | null | undefined) => {
29
+ setBreadcrumbs(prev => prev.map(entry =>
30
+ entry.id === id ? { ...entry, count } : entry
31
+ ));
32
+ }, []);
27
33
 
28
34
  return (
29
35
  <BreadcrumbContext.Provider
30
36
  value={{
31
37
  breadcrumbs,
32
- set
38
+ set,
39
+ updateCount
33
40
  }}
34
41
  >
35
42
  {children}
@@ -1,3 +1,13 @@
1
1
  export * from "./SnackbarProvider";
2
2
  export * from "./ModeController";
3
3
  export * from "./AuthControllerContext";
4
+ export * from "./DataSourceContext";
5
+ export * from "./NavigationContext";
6
+ export * from "./CustomizationControllerContext";
7
+ export * from "./SideEntityControllerContext";
8
+ export * from "./SideDialogsControllerContext";
9
+ export * from "./AnalyticsContext";
10
+ export * from "./StorageSourceContext";
11
+ export * from "./UserConfigurationPersistenceContext";
12
+ export * from "./DialogsProvider";
13
+ export * from "./InternalUserManagementContext";
@@ -57,16 +57,16 @@ export type DefaultAppBarProps<ADDITIONAL_PROPS = object> = {
57
57
 
58
58
  */
59
59
  export const DefaultAppBar = function DefaultAppBar({
60
- title,
61
- endAdornment,
62
- startAdornment,
63
- dropDownActions,
64
- includeModeToggle = true,
65
- className,
66
- style,
67
- user: userProp,
68
- logo: logoProp,
69
- }: DefaultAppBarProps) {
60
+ title,
61
+ endAdornment,
62
+ startAdornment,
63
+ dropDownActions,
64
+ includeModeToggle = true,
65
+ className,
66
+ style,
67
+ user: userProp,
68
+ logo: logoProp,
69
+ }: DefaultAppBarProps) {
70
70
 
71
71
  const {
72
72
  hasDrawer,
@@ -92,7 +92,7 @@ export const DefaultAppBar = function DefaultAppBar({
92
92
 
93
93
  const user = userProp ?? authController.user;
94
94
 
95
- let avatarComponent: JSX.Element | null;
95
+ let avatarComponent: React.ReactElement | null;
96
96
 
97
97
  if (user) {
98
98
  const initial = user?.displayName
@@ -103,7 +103,7 @@ export const DefaultAppBar = function DefaultAppBar({
103
103
  </Avatar>;
104
104
  } else if (user === undefined || authController.initialLoading) {
105
105
  avatarComponent = <div className={"p-1 flex justify-center"}>
106
- <Skeleton className={"w-10 h-10 rounded-full"}/>
106
+ <Skeleton className={"w-10 h-10 rounded-full"} />
107
107
  </div>;
108
108
  } else {
109
109
  avatarComponent = null;
@@ -131,13 +131,13 @@ export const DefaultAppBar = function DefaultAppBar({
131
131
  <div className={"flex flex-row gap-4"}>
132
132
  {!hasDrawer && (logo
133
133
  ? <img src={logo}
134
- alt="Logo"
135
- className={cls("w-[32px] h-[32px] object-contain")}/>
136
- : <FireCMSLogo width={"32px"} height={"32px"}/>)}
134
+ alt="Logo"
135
+ className={cls("w-[32px] h-[32px] object-contain")} />
136
+ : <FireCMSLogo width={"32px"} height={"32px"} />)}
137
137
 
138
138
  {typeof title === "string"
139
139
  ? <Typography variant="subtitle1"
140
- noWrap>
140
+ noWrap>
141
141
  {title}
142
142
  </Typography>
143
143
  : title}
@@ -146,7 +146,7 @@ export const DefaultAppBar = function DefaultAppBar({
146
146
  </div>}
147
147
 
148
148
  {(breadcrumbs.breadcrumbs ?? []).length > 0 && <div className="mr-8 hidden lg:block">
149
- <div className={"flex flex-row gap-2"}>
149
+ <div className={"flex flex-row gap-2 items-center"}>
150
150
  {breadcrumbs.breadcrumbs.map((breadcrumb, index) => {
151
151
  return <React.Fragment key={breadcrumb.url + "_" + index}>
152
152
  <Typography variant={"caption"} color={"secondary"}>
@@ -157,18 +157,31 @@ export const DefaultAppBar = function DefaultAppBar({
157
157
  className="visited:text-inherit visited:dark:text-inherit block"
158
158
  to={breadcrumb.url}
159
159
  >
160
- <Typography variant={"caption"} color={"secondary"}>
161
- {breadcrumb.title}
162
- </Typography>
160
+ <div className="flex flex-row items-center gap-2 whitespace-nowrap">
161
+ <Typography variant={"body2"}>
162
+ {breadcrumb.title}
163
+ </Typography>
164
+ {/* Show count badge for collection breadcrumbs: undefined = not applicable, null = loading, number = count */}
165
+ {breadcrumb.count !== undefined && (
166
+ breadcrumb.count !== null ? (
167
+ <span className="text-xs text-surface-accent-500 dark:text-surface-accent-400 bg-surface-100 dark:bg-surface-700 px-1 py-0 rounded">
168
+ {breadcrumb.count}
169
+ </span>
170
+ ) : (
171
+ <Skeleton className="w-8 h-4 rounded-md" />
172
+ )
173
+ )}
174
+ </div>
163
175
  </Link>
164
176
  </React.Fragment>;
165
177
  })}
166
178
  </div>
167
179
  </div>}
168
180
 
181
+
169
182
  {startAdornment}
170
183
 
171
- <div className={"flex-grow"}/>
184
+ <div className={"flex-grow"} />
172
185
 
173
186
  {endAdornment &&
174
187
  <ErrorBoundary>
@@ -182,13 +195,13 @@ export const DefaultAppBar = function DefaultAppBar({
182
195
  aria-label="Open drawer"
183
196
  size="large">
184
197
  {mode === "dark"
185
- ? <DarkModeIcon/>
186
- : <LightModeIcon/>}
198
+ ? <DarkModeIcon />
199
+ : <LightModeIcon />}
187
200
  </IconButton>}>
188
- <MenuItem onClick={() => setMode("dark")}><DarkModeIcon size={"smallest"}/> Dark</MenuItem>
189
- <MenuItem onClick={() => setMode("light")}><LightModeIcon size={"smallest"}/> Light </MenuItem>
201
+ <MenuItem onClick={() => setMode("dark")}><DarkModeIcon size={"smallest"} /> Dark</MenuItem>
202
+ <MenuItem onClick={() => setMode("light")}><LightModeIcon size={"smallest"} /> Light </MenuItem>
190
203
  <MenuItem onClick={() => setMode("system")}> <BrightnessMediumIcon
191
- size={"smallest"}/>System</MenuItem>
204
+ size={"smallest"} />System</MenuItem>
192
205
  </Menu>}
193
206
 
194
207
  <Menu trigger={avatarComponent}>
@@ -208,7 +221,7 @@ export const DefaultAppBar = function DefaultAppBar({
208
221
  // replace current route with home
209
222
  navigate("/");
210
223
  }}>
211
- <LogoutIcon/>
224
+ <LogoutIcon />
212
225
  Log Out
213
226
  </MenuItem>}
214
227
 
@@ -1,13 +1,13 @@
1
- import React, { useCallback } from "react";
1
+ import React from "react";
2
2
 
3
- import { useLargeLayout, useNavigationController } from "../hooks";
3
+ import { useCollapsedGroups, useLargeLayout, useNavigationController } from "../hooks";
4
4
 
5
5
  import { Link, useNavigate } from "react-router-dom";
6
6
  import { CMSAnalyticsEvent, NavigationEntry, NavigationResult } from "../types";
7
7
  import { IconForView } from "../util";
8
- import { cls, IconButton, Menu, MenuItem, MoreVertIcon, Tooltip, Typography } from "@firecms/ui";
8
+ import { cls, IconButton, Menu, MenuItem, MoreVertIcon, Tooltip } from "@firecms/ui";
9
9
  import { useAnalyticsController } from "../hooks/useAnalyticsController";
10
- import { DrawerNavigationItem } from "./DrawerNavigationItem";
10
+ import { DrawerNavigationGroup } from "./DrawerNavigationGroup";
11
11
  import { FireCMSLogo } from "../components";
12
12
  import { useApp } from "../app/useApp";
13
13
 
@@ -16,9 +16,9 @@ import { useApp } from "../app/useApp";
16
16
  * @group Core
17
17
  */
18
18
  export function DefaultDrawer({
19
- className,
20
- style,
21
- }: {
19
+ className,
20
+ style,
21
+ }: {
22
22
  className?: string
23
23
  style?: React.CSSProperties,
24
24
  }) {
@@ -50,20 +50,10 @@ export function DefaultDrawer({
50
50
  const adminViews = navigationEntries.filter(e => e.type === "admin") ?? [];
51
51
  const groupsWithoutAdmin = groups.filter(g => g !== "Admin");
52
52
 
53
- const buildGroupHeader = useCallback((group?: string) => {
54
- if (!drawerOpen) return <div className="w-full"/>;
55
- return <div
56
- className="pl-6 pr-8 py-4 flex flex-row items-center">
57
- <Typography variant={"caption"}
58
- color={"secondary"}
59
- className="font-medium flex-grow line-clamp-1">
60
- {group ? group.toUpperCase() : "Views".toUpperCase()}
61
- </Typography>
53
+ // Collapsible groups state - using "drawer" namespace for independent state from home page
54
+ const { isGroupCollapsed, toggleGroupCollapsed } = useCollapsedGroups(groupsWithoutAdmin, "drawer");
62
55
 
63
- </div>;
64
- }, [drawerOpen]);
65
-
66
- const onClick = (view: NavigationEntry) => {
56
+ const onItemClick = (view: NavigationEntry) => {
67
57
  const eventName: CMSAnalyticsEvent = view.type === "collection"
68
58
  ? "drawer_navigate_to_collection"
69
59
  : (view.type === "view" ? "drawer_navigate_to_view" : "unmapped_event");
@@ -76,33 +66,29 @@ export function DefaultDrawer({
76
66
  <>
77
67
  <div className={cls("flex flex-col h-full relative flex-grow w-full", className)} style={style}>
78
68
 
79
- <DrawerLogo logo={logo}/>
69
+ <DrawerLogo logo={logo} />
80
70
 
81
71
  <div className={"mt-4 flex-grow overflow-scroll no-scrollbar"}
82
- style={{
83
- maskImage: "linear-gradient(to bottom, transparent 0, black 20px, black calc(100% - 20px), transparent 100%)",
84
- }}>
85
-
86
- {groupsWithoutAdmin.map((group) => (
87
- <div
88
- className={"bg-surface-50 dark:bg-surface-800 dark:bg-opacity-30 my-4 rounded-lg ml-3 mr-1"}
89
- key={`drawer_group_${group}`}>
90
- {buildGroupHeader(group)}
91
- {Object.values(navigationEntries)
92
- .filter(e => e.group === group)
93
- .map((view) =>
94
- <DrawerNavigationItem
95
- key={view.id}
96
- icon={<IconForView collectionOrView={view.collection ?? view.view}
97
- size={"small"}/>}
98
- tooltipsOpen={tooltipsOpen}
99
- adminMenuOpen={adminMenuOpen}
100
- drawerOpen={drawerOpen}
101
- onClick={() => onClick(view)}
102
- url={view.url}
103
- name={view.name}/>)}
104
- </div>
105
- ))}
72
+ style={{
73
+ maskImage: "linear-gradient(to bottom, transparent 0, black 20px, black calc(100% - 20px), transparent 100%)",
74
+ }}>
75
+
76
+ {groupsWithoutAdmin.map((group) => {
77
+ const entriesInGroup = Object.values(navigationEntries).filter(e => e.group === group);
78
+ return (
79
+ <DrawerNavigationGroup
80
+ key={`drawer_group_${group}`}
81
+ group={group}
82
+ entries={entriesInGroup}
83
+ collapsed={isGroupCollapsed(group)}
84
+ onToggleCollapsed={() => toggleGroupCollapsed(group)}
85
+ drawerOpen={drawerOpen}
86
+ tooltipsOpen={tooltipsOpen}
87
+ adminMenuOpen={adminMenuOpen}
88
+ onItemClick={onItemClick}
89
+ />
90
+ );
91
+ })}
106
92
 
107
93
  </div>
108
94
 
@@ -115,9 +101,9 @@ export function DefaultDrawer({
115
101
  shape={"square"}
116
102
  className={"m-4 text-surface-900 dark:text-white w-fit"}>
117
103
  <Tooltip title={"Admin"}
118
- open={tooltipsOpen}
119
- side={"right"} sideOffset={28}>
120
- <MoreVertIcon/>
104
+ open={tooltipsOpen}
105
+ side={"right"} sideOffset={28}>
106
+ <MoreVertIcon />
121
107
  </Tooltip>
122
108
  {drawerOpen && <div
123
109
  className={cls(
@@ -132,10 +118,10 @@ export function DefaultDrawer({
132
118
  <MenuItem
133
119
  onClick={(event) => {
134
120
  event.preventDefault();
135
- navigate(entry.url); // Consistent use of entry.url for navigation
121
+ navigate(entry.url);
136
122
  }}
137
123
  key={entry.id}>
138
- {<IconForView collectionOrView={entry.view}/>}
124
+ {<IconForView collectionOrView={entry.view} />}
139
125
  {entry.name}
140
126
  </MenuItem>)}
141
127
 
@@ -167,17 +153,17 @@ export function DrawerLogo({ logo }: {
167
153
  className={cls("cursor-pointer rounded ml-3 mr-1")}>
168
154
 
169
155
  <Tooltip title={"Home"}
170
- sideOffset={20}
171
- side="right">
156
+ sideOffset={20}
157
+ side="right">
172
158
  <Link
173
159
  className={"block"}
174
160
  to={navigation.basePath}>
175
161
  {logo
176
162
  ? <img src={logo}
177
- alt="Logo"
178
- className={cls("max-w-full max-h-full transition-all object-contain",
179
- drawerOpen ? "w-[96px] h-[96px]" : "w-[32px] h-[32px]")}/>
180
- : <FireCMSLogo/>}
163
+ alt="Logo"
164
+ className={cls("max-w-full max-h-full transition-all object-contain",
165
+ drawerOpen ? "w-[96px] h-[96px]" : "w-[32px] h-[32px]")} />
166
+ : <FireCMSLogo />}
181
167
 
182
168
  </Link>
183
169
  </Tooltip>