@firecms/core 3.0.0-canary.4 → 3.0.0-canary.40

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 (189) hide show
  1. package/README.md +2 -2
  2. package/dist/components/ClearFilterSortButton.d.ts +5 -0
  3. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +1 -1
  4. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
  5. package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +2 -2
  6. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +1 -4
  7. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +1 -2
  8. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +11 -0
  9. package/dist/components/EntityCollectionView/useSelectionController.d.ts +2 -0
  10. package/dist/components/EntityPreview.d.ts +25 -7
  11. package/dist/components/EntityView.d.ts +11 -0
  12. package/dist/components/FieldCaption.d.ts +5 -0
  13. package/dist/components/HomePage/NavigationCard.d.ts +8 -0
  14. package/dist/components/HomePage/{NavigationCollectionCard.d.ts → NavigationCardBinding.d.ts} +2 -2
  15. package/dist/components/HomePage/SmallNavigationCard.d.ts +6 -0
  16. package/dist/components/HomePage/index.d.ts +3 -1
  17. package/dist/components/VirtualTable/VirtualTableProps.d.ts +1 -1
  18. package/dist/components/index.d.ts +4 -3
  19. package/dist/contexts/AuthControllerContext.d.ts +1 -1
  20. package/dist/{internal/EntityView.d.ts → core/EntityEditView.d.ts} +2 -2
  21. package/dist/core/SideEntityView.d.ts +7 -0
  22. package/dist/core/index.d.ts +0 -2
  23. package/dist/form/EntityForm.d.ts +1 -1
  24. package/dist/form/components/StorageItemPreview.d.ts +3 -2
  25. package/dist/form/components/StorageUploadProgress.d.ts +1 -1
  26. package/dist/form/components/index.d.ts +1 -0
  27. package/dist/form/field_bindings/KeyValueFieldBinding.d.ts +1 -1
  28. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  29. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +4 -3
  30. package/dist/form/field_bindings/TextFieldBinding.d.ts +2 -2
  31. package/dist/form/index.d.ts +1 -0
  32. package/dist/form/validation.d.ts +1 -1
  33. package/dist/hooks/data/delete.d.ts +2 -2
  34. package/dist/hooks/data/save.d.ts +1 -1
  35. package/dist/hooks/data/useDataSource.d.ts +2 -2
  36. package/dist/hooks/data/useEntityFetch.d.ts +3 -3
  37. package/dist/hooks/index.d.ts +3 -1
  38. package/dist/{core → hooks}/useBuildModeController.d.ts +1 -1
  39. package/dist/hooks/useBuildNavigationController.d.ts +6 -4
  40. package/dist/hooks/useProjectLog.d.ts +6 -2
  41. package/dist/hooks/useStorageSource.d.ts +2 -2
  42. package/dist/hooks/useValidateAuthenticator.d.ts +25 -0
  43. package/dist/index.es.js +8343 -7846
  44. package/dist/index.es.js.map +1 -1
  45. package/dist/index.umd.js +5 -5
  46. package/dist/index.umd.js.map +1 -1
  47. package/dist/internal/useBuildDataSource.d.ts +4 -0
  48. package/dist/preview/PropertyPreview.d.ts +1 -1
  49. package/dist/preview/PropertyPreviewProps.d.ts +1 -4
  50. package/dist/preview/components/BooleanPreview.d.ts +5 -1
  51. package/dist/preview/components/EnumValuesChip.d.ts +1 -1
  52. package/dist/preview/components/ReferencePreview.d.ts +1 -7
  53. package/dist/types/analytics.d.ts +1 -1
  54. package/dist/types/auth.d.ts +37 -1
  55. package/dist/types/collections.d.ts +22 -5
  56. package/dist/types/datasource.d.ts +1 -1
  57. package/dist/types/entities.d.ts +1 -1
  58. package/dist/types/entity_callbacks.d.ts +2 -2
  59. package/dist/types/entity_overrides.d.ts +6 -0
  60. package/dist/types/index.d.ts +2 -0
  61. package/dist/types/navigation.d.ts +14 -13
  62. package/dist/types/permissions.d.ts +5 -1
  63. package/dist/types/plugins.d.ts +20 -20
  64. package/dist/types/properties.d.ts +2 -2
  65. package/dist/types/property_config.d.ts +2 -2
  66. package/dist/types/roles.d.ts +31 -0
  67. package/dist/types/storage.d.ts +11 -3
  68. package/dist/types/user.d.ts +5 -0
  69. package/dist/util/collections.d.ts +9 -1
  70. package/dist/util/entities.d.ts +1 -1
  71. package/dist/util/icons.d.ts +8 -2
  72. package/dist/util/permissions.d.ts +4 -4
  73. package/dist/util/references.d.ts +4 -2
  74. package/dist/util/resolutions.d.ts +1 -1
  75. package/dist/util/useTraceUpdate.d.ts +1 -0
  76. package/package.json +24 -24
  77. package/src/components/ClearFilterSortButton.tsx +41 -0
  78. package/src/components/DeleteEntityDialog.tsx +4 -4
  79. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  80. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +268 -277
  81. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +1 -1
  82. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +13 -13
  83. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +9 -16
  84. package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +3 -3
  85. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +27 -32
  86. package/src/components/EntityCollectionTable/internal/default_entity_actions.tsx +9 -5
  87. package/src/components/EntityCollectionView/EntityCollectionView.tsx +39 -49
  88. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +5 -6
  89. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +68 -0
  90. package/src/components/EntityCollectionView/useSelectionController.tsx +30 -0
  91. package/src/components/EntityPreview.tsx +207 -70
  92. package/src/components/EntityView.tsx +84 -0
  93. package/src/components/FieldCaption.tsx +14 -0
  94. package/src/components/FireCMSAppBar.tsx +8 -0
  95. package/src/components/HomePage/DefaultHomePage.tsx +14 -10
  96. package/src/components/HomePage/NavigationCard.tsx +69 -0
  97. package/src/components/HomePage/NavigationCardBinding.tsx +116 -0
  98. package/src/components/HomePage/SmallNavigationCard.tsx +45 -0
  99. package/src/components/HomePage/index.tsx +3 -1
  100. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -4
  101. package/src/components/ReferenceWidget.tsx +4 -4
  102. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +23 -8
  103. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +35 -24
  104. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +35 -15
  105. package/src/components/VirtualTable/VirtualTableProps.tsx +1 -1
  106. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +1 -1
  107. package/src/components/common/useDataSourceEntityCollectionTableController.tsx +1 -1
  108. package/src/components/index.tsx +4 -3
  109. package/src/contexts/AuthControllerContext.tsx +1 -1
  110. package/src/core/Drawer.tsx +66 -39
  111. package/src/{internal/EntityView.tsx → core/EntityEditView.tsx} +22 -39
  112. package/src/core/EntitySidePanel.tsx +2 -2
  113. package/src/core/FireCMS.tsx +18 -2
  114. package/src/core/NavigationRoutes.tsx +8 -0
  115. package/src/core/SideEntityView.tsx +38 -0
  116. package/src/core/field_configs.tsx +1 -2
  117. package/src/core/index.tsx +0 -2
  118. package/src/form/EntityForm.tsx +20 -12
  119. package/src/form/components/StorageItemPreview.tsx +5 -3
  120. package/src/form/components/StorageUploadProgress.tsx +6 -5
  121. package/src/form/components/index.tsx +1 -0
  122. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -3
  123. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +12 -15
  124. package/src/form/field_bindings/BlockFieldBinding.tsx +2 -3
  125. package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -4
  126. package/src/form/field_bindings/KeyValueFieldBinding.tsx +18 -18
  127. package/src/form/field_bindings/MapFieldBinding.tsx +17 -17
  128. package/src/form/field_bindings/MarkdownFieldBinding.tsx +1 -2
  129. package/src/form/field_bindings/MultiSelectBinding.tsx +2 -3
  130. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -3
  131. package/src/form/field_bindings/ReferenceFieldBinding.tsx +5 -3
  132. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
  133. package/src/form/field_bindings/SelectFieldBinding.tsx +2 -3
  134. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +15 -6
  135. package/src/form/field_bindings/SwitchFieldBinding.tsx +2 -3
  136. package/src/form/field_bindings/TextFieldBinding.tsx +10 -9
  137. package/src/form/index.tsx +1 -0
  138. package/src/form/validation.ts +3 -4
  139. package/src/hooks/data/delete.ts +3 -3
  140. package/src/hooks/data/save.ts +1 -1
  141. package/src/hooks/data/useCollectionFetch.tsx +1 -1
  142. package/src/hooks/data/useDataSource.tsx +8 -3
  143. package/src/hooks/data/useEntityFetch.tsx +4 -4
  144. package/src/hooks/index.tsx +5 -1
  145. package/src/{core → hooks}/useBuildLocalConfigurationPersistence.tsx +9 -10
  146. package/src/{core → hooks}/useBuildModeController.tsx +12 -6
  147. package/src/hooks/useBuildNavigationController.tsx +190 -72
  148. package/src/hooks/useProjectLog.tsx +16 -6
  149. package/src/hooks/useReferenceDialog.tsx +2 -2
  150. package/src/hooks/useStorageSource.tsx +7 -2
  151. package/src/hooks/useValidateAuthenticator.tsx +135 -0
  152. package/src/internal/useBuildDataSource.ts +6 -1
  153. package/src/internal/useBuildSideEntityController.tsx +18 -12
  154. package/src/preview/PropertyPreview.tsx +1 -1
  155. package/src/preview/PropertyPreviewProps.tsx +1 -11
  156. package/src/preview/components/BooleanPreview.tsx +19 -4
  157. package/src/preview/components/EnumValuesChip.tsx +1 -1
  158. package/src/preview/components/ReferencePreview.tsx +55 -147
  159. package/src/preview/property_previews/StringPropertyPreview.tsx +8 -7
  160. package/src/types/analytics.ts +1 -0
  161. package/src/types/auth.tsx +50 -1
  162. package/src/types/collections.ts +24 -5
  163. package/src/types/datasource.ts +1 -1
  164. package/src/types/entities.ts +1 -1
  165. package/src/types/entity_actions.tsx +4 -0
  166. package/src/types/entity_callbacks.ts +2 -2
  167. package/src/types/entity_overrides.tsx +7 -0
  168. package/src/types/firecms.tsx +0 -1
  169. package/src/types/index.ts +2 -0
  170. package/src/types/navigation.ts +17 -16
  171. package/src/types/permissions.ts +6 -1
  172. package/src/types/plugins.tsx +26 -28
  173. package/src/types/properties.ts +3 -2
  174. package/src/types/property_config.tsx +2 -2
  175. package/src/types/roles.ts +41 -0
  176. package/src/types/side_entity_controller.tsx +1 -0
  177. package/src/types/storage.ts +12 -3
  178. package/src/types/user.ts +7 -0
  179. package/src/util/collections.ts +22 -0
  180. package/src/util/entities.ts +1 -1
  181. package/src/util/icons.tsx +11 -3
  182. package/src/util/permissions.ts +11 -8
  183. package/src/util/references.ts +36 -5
  184. package/src/util/strings.ts +2 -2
  185. package/src/util/useTraceUpdate.tsx +2 -1
  186. package/src/components/HomePage/NavigationCollectionCard.tsx +0 -146
  187. /package/dist/{components → form/components}/LabelWithIcon.d.ts +0 -0
  188. /package/dist/{core → hooks}/useBuildLocalConfigurationPersistence.d.ts +0 -0
  189. /package/src/{components → form/components}/LabelWithIcon.tsx +0 -0
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
- import { useLocation } from "react-router-dom";
3
2
  import equal from "react-fast-compare"
4
3
 
5
4
  import {
@@ -11,12 +10,14 @@ import {
11
10
  EntityCollectionsBuilder,
12
11
  EntityReference,
13
12
  NavigationController,
13
+ PermissionsBuilder,
14
14
  TopNavigationEntry,
15
15
  TopNavigationResult,
16
16
  User,
17
17
  UserConfigurationPersistence
18
18
  } from "../types";
19
19
  import {
20
+ applyPermissionsFunctionIfEmpty,
20
21
  getCollectionByPathOrId,
21
22
  mergeDeep,
22
23
  removeInitialAndTrailingSlashes,
@@ -28,12 +29,15 @@ import { getParentReferencesFromPath } from "../util/parent_references_from_path
28
29
  const DEFAULT_BASE_PATH = "/";
29
30
  const DEFAULT_COLLECTION_PATH = "/c";
30
31
 
31
- type BuildNavigationContextProps<EC extends EntityCollection, UserType extends User> = {
32
+ export type BuildNavigationContextProps<EC extends EntityCollection, UserType extends User> = {
32
33
  basePath?: string,
33
34
  baseCollectionPath?: string,
34
35
  authController: AuthController<UserType>;
35
36
  collections?: EC[] | EntityCollectionsBuilder<EC>;
37
+ collectionPermissions?: PermissionsBuilder;
36
38
  views?: CMSView[] | CMSViewsBuilder;
39
+ adminViews?: CMSView[] | CMSViewsBuilder;
40
+ viewsOrder?: string[];
37
41
  userConfigPersistence?: UserConfigurationPersistence;
38
42
  dataSourceDelegate: DataSourceDelegate;
39
43
  /**
@@ -46,22 +50,25 @@ type BuildNavigationContextProps<EC extends EntityCollection, UserType extends U
46
50
  injectCollections?: (collections: EntityCollection[]) => EntityCollection[];
47
51
  };
48
52
 
49
- export function useBuildNavigationController<EC extends EntityCollection, UserType extends User>({
50
- basePath = DEFAULT_BASE_PATH,
51
- baseCollectionPath = DEFAULT_COLLECTION_PATH,
52
- authController,
53
- collections: collectionsProp,
54
- views: baseViews,
55
- userConfigPersistence,
56
- dataSourceDelegate,
57
- injectCollections
58
- }: BuildNavigationContextProps<EC, UserType>): NavigationController {
59
-
60
- const location = useLocation();
61
-
62
- const collectionsRef = useRef<EntityCollection[] | null>();
63
- const [collections, setCollections] = useState<EntityCollection[] | undefined>();
64
- const [views, setViews] = useState<CMSView[] | undefined>();
53
+ export function useBuildNavigationController<EC extends EntityCollection, UserType extends User>(props: BuildNavigationContextProps<EC, UserType>): NavigationController {
54
+ const {
55
+ basePath = DEFAULT_BASE_PATH,
56
+ baseCollectionPath = DEFAULT_COLLECTION_PATH,
57
+ authController,
58
+ collections: collectionsProp,
59
+ collectionPermissions,
60
+ views: viewsProp,
61
+ adminViews: adminViewsProp,
62
+ viewsOrder,
63
+ userConfigPersistence,
64
+ dataSourceDelegate,
65
+ injectCollections
66
+ } = props;
67
+
68
+ const collectionsRef = useRef<EntityCollection[] | undefined>();
69
+ const viewsRef = useRef<CMSView[] | undefined>();
70
+ const adminViewsRef = useRef<CMSView[] | undefined>();
71
+
65
72
  const [initialised, setInitialised] = useState<boolean>(false);
66
73
 
67
74
  const [topLevelNavigation, setTopLevelNavigation] = useState<TopNavigationResult | undefined>(undefined);
@@ -81,18 +88,18 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
81
88
  const buildUrlCollectionPath = useCallback((path: string): string => `${removeInitialAndTrailingSlashes(baseCollectionPath)}/${encodePath(path)}`,
82
89
  [baseCollectionPath]);
83
90
 
84
- const computeTopNavigation = useCallback((collections: EntityCollection[], views: CMSView[]): TopNavigationResult => {
85
- const navigationEntries: TopNavigationEntry[] = [
91
+ const computeTopNavigation = useCallback((collections: EntityCollection[], views: CMSView[], adminViews: CMSView[], viewsOrder?: string[]): TopNavigationResult => {
92
+ let navigationEntries: TopNavigationEntry[] = [
86
93
  ...(collections ?? []).map(collection => (!collection.hideFromNavigation
87
- ? {
94
+ ? ({
88
95
  url: buildUrlCollectionPath(collection.id ?? collection.path),
89
96
  type: "collection",
90
97
  name: collection.name.trim(),
91
98
  path: collection.id ?? collection.path,
92
99
  collection,
93
100
  description: collection.description?.trim(),
94
- group: collection.group?.trim()
95
- }
101
+ group: getGroup(collection)
102
+ } satisfies TopNavigationEntry)
96
103
  : undefined))
97
104
  .filter(Boolean) as TopNavigationEntry[],
98
105
  ...(views ?? []).map(view =>
@@ -101,18 +108,74 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
101
108
  url: buildCMSUrlPath(Array.isArray(view.path) ? view.path[0] : view.path),
102
109
  name: view.name.trim(),
103
110
  type: "view",
111
+ path: view.path,
104
112
  view,
105
113
  description: view.description?.trim(),
106
- group: view.group?.trim()
107
- })
114
+ group: getGroup(view)
115
+ } satisfies TopNavigationEntry)
116
+ : undefined)
117
+ .filter(Boolean) as TopNavigationEntry[],
118
+ ...(adminViews ?? []).map(view =>
119
+ !view.hideFromNavigation
120
+ ? ({
121
+ url: buildCMSUrlPath(Array.isArray(view.path) ? view.path[0] : view.path),
122
+ name: view.name.trim(),
123
+ type: "admin",
124
+ path: view.path,
125
+ view,
126
+ description: view.description?.trim(),
127
+ group: "Admin"
128
+ } satisfies TopNavigationEntry)
108
129
  : undefined)
109
130
  .filter(Boolean) as TopNavigationEntry[]
110
131
  ];
111
132
 
133
+ // Sort by group, entries with group "Admin" will go last, and second to last will be the group "Views"
134
+ navigationEntries = navigationEntries.sort((a, b) => {
135
+ if (a.group !== "Views" && a.group !== "Admin" && (b.group === "Views" || b.group === "Admin")) {
136
+ return -1;
137
+ }
138
+ if (b.group !== "Views" && b.group !== "Admin" && (a.group === "Views" || a.group === "Admin")) {
139
+ return 1;
140
+ }
141
+ if (a.group === "Admin" && b.group !== "Admin") {
142
+ return 1;
143
+ }
144
+ if (a.group !== "Admin" && b.group === "Admin") {
145
+ return -1;
146
+ }
147
+ if (a.group === "Views" && b.group !== "Views") {
148
+ return -1;
149
+ }
150
+ if (a.group !== "Views" && b.group === "Views") {
151
+ return 1;
152
+ }
153
+ return 0;
154
+
155
+ });
156
+
157
+ if (viewsOrder) {
158
+ navigationEntries = navigationEntries.sort((a, b) => {
159
+ const aIndex = viewsOrder.indexOf(a.path);
160
+ const bIndex = viewsOrder.indexOf(b.path);
161
+ if (aIndex === -1 && bIndex === -1) {
162
+ return 0;
163
+ }
164
+ if (aIndex === -1) {
165
+ return 1;
166
+ }
167
+ if (bIndex === -1) {
168
+ return -1;
169
+ }
170
+ return aIndex - bIndex;
171
+ });
172
+ }
173
+
112
174
  const groups: string[] = Object.values(navigationEntries)
113
175
  .map(e => e.group)
114
176
  .filter(Boolean)
115
177
  .filter((value, index, array) => array.indexOf(value) === index) as string[];
178
+
116
179
  return {
117
180
  navigationEntries,
118
181
  groups
@@ -124,17 +187,34 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
124
187
  if (authController.initialLoading)
125
188
  return;
126
189
 
190
+ console.debug("Refreshing navigation");
191
+
127
192
  try {
128
- const [resolvedCollections = [], resolvedViews = []] = await Promise.all([
129
- resolveCollections(collectionsProp, authController, dataSourceDelegate, injectCollections),
130
- resolveCMSViews(baseViews, authController, dataSourceDelegate)
193
+
194
+ const [resolvedCollections = [], resolvedViews, resolvedAdminViews = []] = await Promise.all([
195
+ resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate, injectCollections),
196
+ resolveCMSViews(viewsProp, authController, dataSourceDelegate),
197
+ resolveCMSViews(adminViewsProp, authController, dataSourceDelegate)
131
198
  ]
132
199
  );
133
- if (!equal(collectionsRef.current, resolvedCollections) || !equal(views, resolvedViews) || !equal(topLevelNavigation, computeTopNavigation(resolvedCollections, resolvedViews))) {
200
+
201
+ let shouldUpdateTopLevelNav = false;
202
+ if (!areCollectionListsEqual(collectionsRef.current ?? [], resolvedCollections)) {
134
203
  collectionsRef.current = resolvedCollections;
135
- setCollections(resolvedCollections);
136
- setViews(resolvedViews);
137
- setTopLevelNavigation(computeTopNavigation(resolvedCollections ?? [], resolvedViews));
204
+ shouldUpdateTopLevelNav = true;
205
+ }
206
+ if (!equal(viewsRef.current, resolvedViews)) {
207
+ viewsRef.current = resolvedViews;
208
+ shouldUpdateTopLevelNav = true;
209
+ }
210
+ if (!equal(adminViewsRef.current, resolvedAdminViews)) {
211
+ adminViewsRef.current = resolvedAdminViews;
212
+ shouldUpdateTopLevelNav = true;
213
+ }
214
+
215
+ const computedTopLevelNav = computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder);
216
+ if (shouldUpdateTopLevelNav && !equal(topLevelNavigation, computedTopLevelNav)) {
217
+ setTopLevelNavigation(computedTopLevelNav);
138
218
  }
139
219
  } catch (e) {
140
220
  console.error(e);
@@ -143,25 +223,34 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
143
223
 
144
224
  setNavigationLoading(false);
145
225
  setInitialised(true);
146
- }, [collectionsProp, authController.user, authController.initialLoading, baseViews, computeTopNavigation, injectCollections]);
226
+
227
+ }, [
228
+ collectionsProp,
229
+ collectionPermissions,
230
+ authController.user,
231
+ authController.initialLoading,
232
+ viewsProp,
233
+ adminViewsProp,
234
+ computeTopNavigation,
235
+ injectCollections
236
+ ]);
147
237
 
148
238
  useEffect(() => {
149
239
  refreshNavigation();
150
240
  }, [refreshNavigation]);
151
241
 
152
- const getCollection = useCallback(<EC extends EntityCollection>(
242
+ const getCollection = useCallback((
153
243
  idOrPath: string,
154
244
  entityId?: string,
155
245
  includeUserOverride = false
156
246
  ): EC | undefined => {
157
-
247
+ const collections = collectionsRef.current;
158
248
  if (!collections)
159
249
  return undefined;
160
250
 
161
251
  const baseCollection = getCollectionByPathOrId(removeInitialAndTrailingSlashes(idOrPath), collections);
162
252
 
163
253
  const userOverride = includeUserOverride ? userConfigPersistence?.getCollectionConfig(idOrPath) : undefined;
164
-
165
254
  const overriddenCollection = baseCollection ? mergeDeep(baseCollection, userOverride) : undefined;
166
255
 
167
256
  let result: Partial<EntityCollection> | undefined = overriddenCollection;
@@ -182,14 +271,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
182
271
 
183
272
  return { ...overriddenCollection, ...result } as EC;
184
273
 
185
- }, [
186
- basePath,
187
- baseCollectionPath,
188
- collections,
189
- ]);
274
+ }, [userConfigPersistence]);
190
275
 
191
276
  const getCollectionFromPaths = useCallback(<EC extends EntityCollection>(pathSegments: string[]): EC | undefined => {
192
- let currentCollections = collections;
277
+
278
+ const collections = collectionsRef.current;
279
+ let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
193
280
  if (!currentCollections)
194
281
  throw Error("Collections have not been initialised yet");
195
282
 
@@ -205,10 +292,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
205
292
 
206
293
  return undefined;
207
294
 
208
- }, [collections]);
295
+ }, []);
209
296
 
210
297
  const getCollectionFromIds = useCallback(<EC extends EntityCollection>(ids: string[]): EC | undefined => {
211
- let currentCollections = collections;
298
+
299
+ const collections = collectionsRef.current;
300
+ let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
212
301
  if (!currentCollections)
213
302
  throw Error("Collections have not been initialised yet");
214
303
 
@@ -224,7 +313,7 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
224
313
 
225
314
  return undefined;
226
315
 
227
- }, [collections]);
316
+ }, []);
228
317
 
229
318
  const isUrlCollectionPath = useCallback(
230
319
  (path: string): boolean => removeInitialAndTrailingSlashes(path + "/").startsWith(removeInitialAndTrailingSlashes(fullCollectionPath) + "/"),
@@ -246,25 +335,19 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
246
335
  []);
247
336
 
248
337
  const resolveAliasesFrom = useCallback((path: string): string => {
338
+ const collections = collectionsRef.current;
249
339
  if (!collections)
250
340
  throw Error("Collections have not been initialised yet");
251
341
  return resolveCollectionPathIds(path, collections);
252
- }, [collections]);
253
-
254
- const state = location.state as any;
255
- /**
256
- * The location can be overridden if `base_location` is set in the
257
- * state field of the current location. This can happen if you open
258
- * a side entity, like `products`, from a different one, like `users`
259
- */
260
- const baseLocation = state && state.base_location ? state.base_location : location;
342
+ }, []);
261
343
 
262
344
  const getAllParentReferencesForPath = useCallback((path: string): EntityReference[] => {
345
+ const collections = collectionsRef.current ?? [];
263
346
  return getParentReferencesFromPath({
264
347
  path,
265
348
  collections
266
349
  });
267
- }, [collections]);
350
+ }, []);
268
351
 
269
352
  const getParentCollectionIds = useCallback((path: string): string[] => {
270
353
 
@@ -283,23 +366,24 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
283
366
  }, [getAllParentReferencesForPath])
284
367
 
285
368
  const convertIdsToPaths = useCallback((ids: string[]): string[] => {
286
- let currentCollections = collections;
287
- const paths: string[] = [];
288
- for (let i = 0; i < ids.length; i++) {
289
- const id = ids[i];
290
- const collection: EntityCollection | undefined = currentCollections!.find(c => c.id === id);
291
- if (!collection)
292
- throw Error(`Collection with id ${id} not found`);
293
- paths.push(collection.path);
294
- currentCollections = collection.subcollections;
295
- }
296
- return paths;
369
+ const collections = collectionsRef.current;
370
+ let currentCollections = collections;
371
+ const paths: string[] = [];
372
+ for (let i = 0; i < ids.length; i++) {
373
+ const id = ids[i];
374
+ const collection: EntityCollection | undefined = currentCollections!.find(c => c.id === id);
375
+ if (!collection)
376
+ throw Error(`Collection with id ${id} not found`);
377
+ paths.push(collection.path);
378
+ currentCollections = collection.subcollections;
297
379
  }
298
- , [getCollectionFromIds]);
380
+ return paths;
381
+ }, [getCollectionFromIds]);
299
382
 
300
383
  return {
301
- collections,
302
- views,
384
+ collections: collectionsRef.current,
385
+ views: viewsRef.current,
386
+ adminViews: adminViewsRef.current,
303
387
  loading: !initialised || navigationLoading,
304
388
  navigationLoadingError,
305
389
  homeUrl,
@@ -316,7 +400,6 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
316
400
  buildCMSUrlPath,
317
401
  resolveAliasesFrom,
318
402
  topLevelNavigation,
319
- baseLocation,
320
403
  refreshNavigation,
321
404
  getParentReferencesFromPath: getAllParentReferencesForPath,
322
405
  getParentCollectionIds,
@@ -341,8 +424,8 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
341
424
  return resolvedCollections
342
425
  .filter((c) => {
343
426
  if (!c.permissions) return true;
344
- const resolvedPermissions = resolvePermissions(c, authController, [c.path], null,)
345
- return resolvedPermissions.read !== false;
427
+ const resolvedPermissions = resolvePermissions(c, authController, c.path, null)
428
+ return resolvedPermissions?.read !== false;
346
429
  })
347
430
  .map((c) => {
348
431
  if (!c.subcollections) return c;
@@ -354,6 +437,7 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
354
437
  }
355
438
 
356
439
  async function resolveCollections(collections: undefined | EntityCollection[] | EntityCollectionsBuilder<any>,
440
+ collectionPermissions: PermissionsBuilder | undefined,
357
441
  authController: AuthController,
358
442
  dataSource: DataSourceDelegate,
359
443
  injectCollections?: (collections: EntityCollection[]) => EntityCollection[]) {
@@ -368,6 +452,8 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
368
452
  resolvedCollections = collections;
369
453
  }
370
454
 
455
+ resolvedCollections = applyPermissionsFunctionIfEmpty(resolvedCollections, collectionPermissions);
456
+
371
457
  resolvedCollections = filterOutNotAllowedCollections(resolvedCollections, authController);
372
458
 
373
459
  if (injectCollections) {
@@ -390,3 +476,35 @@ async function resolveCMSViews(baseViews: CMSView[] | CMSViewsBuilder | undefine
390
476
  }
391
477
  return resolvedViews;
392
478
  }
479
+
480
+ function getGroup(collectionOrView: EntityCollection<any, any> | CMSView) {
481
+ const trimmed = collectionOrView.group?.trim();
482
+ if (!trimmed || trimmed === "") {
483
+ return "Views";
484
+ }
485
+ return trimmed ?? "Views";
486
+ }
487
+
488
+ function areCollectionListsEqual(a: EntityCollection[], b: EntityCollection[]) {
489
+ if (a.length !== b.length) {
490
+ return false;
491
+ }
492
+ const aSorted = a.sort((a, b) => a.id.localeCompare(b.id));
493
+ const bSorted = b.sort((a, b) => a.id.localeCompare(b.id));
494
+ return aSorted.every((value, index) => areCollectionsEqual(value, bSorted[index]));
495
+ }
496
+
497
+ function areCollectionsEqual(a: EntityCollection, b: EntityCollection) {
498
+ const {
499
+ subcollections: subcollectionsA,
500
+ ...restA
501
+ } = a;
502
+ const {
503
+ subcollections: subcollectionsB,
504
+ ...restB
505
+ } = b;
506
+ if (!areCollectionListsEqual(subcollectionsA ?? [], subcollectionsB ?? [])) {
507
+ return false;
508
+ }
509
+ return equal(restA, restB);
510
+ }
@@ -1,10 +1,15 @@
1
- import { useEffect, useRef } from "react";
2
- import { AuthController } from "../types";
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { AuthController, FireCMSPlugin } from "../types";
3
3
 
4
4
  export const DEFAULT_SERVER_DEV = "https://api-kdoe6pj3qq-ey.a.run.app";
5
5
  export const DEFAULT_SERVER = "https://api-drplyi3b6q-ey.a.run.app";
6
6
 
7
- async function makeRequest(authController: AuthController) {
7
+ export type AccessResponse = {
8
+ blocked?: boolean;
9
+ message?: string;
10
+ }
11
+
12
+ async function makeRequest(authController: AuthController, pluginKeys: string | undefined) {
8
13
  const firebaseToken = await authController.getAuthToken();
9
14
  return fetch(DEFAULT_SERVER + "/access_log",
10
15
  {
@@ -14,18 +19,23 @@ async function makeRequest(authController: AuthController) {
14
19
  "Content-Type": "application/json",
15
20
  Authorization: `Basic ${firebaseToken}`,
16
21
  },
17
- body: JSON.stringify({})
22
+ body: JSON.stringify({ plugins: pluginKeys })
18
23
  })
19
24
  .then(async (res) => {
25
+ return res.json();
20
26
  });
21
27
  }
22
28
 
23
- export function useProjectLog(authController: AuthController) {
29
+ export function useProjectLog(authController: AuthController,
30
+ plugins?: FireCMSPlugin<any, any, any>[]): AccessResponse | null {
31
+ const [accessResponse, setAccessResponse] = useState<AccessResponse | null>(null);
24
32
  const accessedUserRef = useRef<string | null>(null);
33
+ const pluginKeys = plugins?.map(plugin => plugin.key).join(",");
25
34
  useEffect(() => {
26
35
  if (authController.user && authController.user.uid !== accessedUserRef.current && !authController.initialLoading) {
27
- makeRequest(authController);
36
+ makeRequest(authController, pluginKeys).then(setAccessResponse);
28
37
  accessedUserRef.current = authController.user.uid;
29
38
  }
30
39
  }, [authController]);
40
+ return accessResponse;
31
41
  }
@@ -1,5 +1,5 @@
1
1
  import { useSideDialogsController } from "./useSideDialogsController";
2
- import { ReferenceSelectionTable, ReferenceSelectionInnerProps } from "../components";
2
+ import { ReferenceSelectionInnerProps, ReferenceSelectionTable } from "../components";
3
3
  import { useCallback } from "react";
4
4
  import { useNavigationController } from "./useNavigationController";
5
5
 
@@ -27,7 +27,7 @@ export function useReferenceDialog<M extends Record<string, any>>(referenceDialo
27
27
  if (!usedCollection)
28
28
  usedCollection = navigation.getCollection(referenceDialogProps.path);
29
29
  if (!usedCollection)
30
- throw Error("Not able to resolve the collection in useReferenceDialog");
30
+ throw Error("Not able to resolve the collection in useReferenceDialog. Make sure a collection is registered in path " + referenceDialogProps.path);
31
31
  sideDialogsController.open({
32
32
  key: `reference_${referenceDialogProps.path}`,
33
33
  component:
@@ -1,4 +1,4 @@
1
- import { StorageSource } from "../types";
1
+ import { EntityCollection, StorageSource } from "../types";
2
2
  import { StorageSourceContext } from "../contexts/StorageSourceContext";
3
3
  import { useContext } from "react";
4
4
 
@@ -6,4 +6,9 @@ import { useContext } from "react";
6
6
  * Use this hook to get the storage source being used
7
7
  * @group Hooks and utilities
8
8
  */
9
- export const useStorageSource = (): StorageSource => useContext(StorageSourceContext);
9
+ export const useStorageSource = (collection?: EntityCollection): StorageSource => {
10
+ const defaultStorageSource = useContext(StorageSourceContext);
11
+ if (collection?.overrides?.storageSource)
12
+ return collection.overrides.storageSource;
13
+ return defaultStorageSource;
14
+ };
@@ -0,0 +1,135 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import equal from "react-fast-compare";
3
+
4
+ import { AppCheckTokenResult, AuthController, Authenticator, DataSourceDelegate, StorageSource, User } from "../index";
5
+
6
+ /**
7
+ * This hook is used internally for validating an authenticator.
8
+ *
9
+ * @param authController
10
+ * @param authentication
11
+ * @param getAppCheckToken
12
+ * @param appCheckForceRefresh
13
+ * @param storageSource
14
+ * @param dataSourceDelegate
15
+ */
16
+ export function useValidateAuthenticator<UserType extends User = User, Controller extends AuthController<UserType> = AuthController<UserType>>({
17
+ disabled,
18
+ authController,
19
+ authenticator,
20
+ getAppCheckToken,
21
+ appCheckForceRefresh = false,
22
+ storageSource,
23
+ dataSourceDelegate
24
+ }:
25
+ {
26
+ disabled?: boolean,
27
+ authController: Controller,
28
+ authenticator?: boolean | Authenticator<UserType, Controller>,
29
+ getAppCheckToken?: (forceRefresh: boolean) => Promise<AppCheckTokenResult> | undefined,
30
+ appCheckForceRefresh?: boolean,
31
+ dataSourceDelegate: DataSourceDelegate;
32
+ storageSource: StorageSource;
33
+ }): {
34
+ canAccessMainView: boolean,
35
+ authLoading: boolean,
36
+ notAllowedError: any,
37
+ authVerified: boolean,
38
+ } {
39
+
40
+ const authenticationEnabled = Boolean(authenticator);
41
+
42
+ const [authLoading, setAuthLoading] = useState<boolean>(authenticationEnabled);
43
+ const [notAllowedError, setNotAllowedError] = useState<any>(false);
44
+ const [authVerified, setAuthVerified] = useState<boolean>(!authenticationEnabled || Boolean(authController.loginSkipped));
45
+
46
+ const canAccessMainView = (authVerified) &&
47
+ (!authenticationEnabled || Boolean(authController.user) || Boolean(authController.loginSkipped)) &&
48
+ !notAllowedError;
49
+
50
+ useEffect(() => {
51
+ if (authController.loginSkipped)
52
+ setAuthVerified(true);
53
+ }, [authController.loginSkipped]);
54
+
55
+ /**
56
+ * We use this ref to check the authentication only if the user has
57
+ * changed.
58
+ */
59
+ const checkedUserRef = useRef<User | undefined>();
60
+
61
+ const checkAuthentication = useCallback(async () => {
62
+
63
+ if (disabled) {
64
+ return;
65
+ }
66
+
67
+ if (authController.initialLoading) {
68
+ return;
69
+ }
70
+
71
+ if (!authController.user && !authController.loginSkipped) {
72
+ checkedUserRef.current = undefined;
73
+ setAuthLoading(false);
74
+ setAuthVerified(false);
75
+ return;
76
+ }
77
+
78
+ const delegateUser = authController.user;
79
+ console.debug("Checking authentication for user", delegateUser);
80
+
81
+ if (getAppCheckToken) {
82
+ try {
83
+ if (!await getAppCheckToken(appCheckForceRefresh)) {
84
+ setNotAllowedError("App Check failed.");
85
+ authController.signOut();
86
+ } else {
87
+ console.debug("App Check success.");
88
+ }
89
+ } catch (e: any) {
90
+ setNotAllowedError(e.message);
91
+ authController.signOut();
92
+ }
93
+ }
94
+
95
+ if (authenticator instanceof Function && delegateUser && !equal(checkedUserRef.current?.uid, delegateUser.uid)) {
96
+ setAuthLoading(true);
97
+ try {
98
+ const allowed = await authenticator({
99
+ user: delegateUser,
100
+ authController,
101
+ dataSourceDelegate,
102
+ storageSource
103
+ });
104
+ if (!allowed) {
105
+ authController.signOut();
106
+ setNotAllowedError(true);
107
+ }
108
+ } catch (e) {
109
+ setNotAllowedError(e);
110
+ authController.signOut();
111
+ }
112
+ setAuthLoading(false);
113
+ setAuthVerified(true);
114
+ checkedUserRef.current = delegateUser;
115
+ } else {
116
+ setAuthLoading(false);
117
+ }
118
+
119
+ if (!authController.initialLoading && !delegateUser) {
120
+ setAuthVerified(true);
121
+ }
122
+
123
+ }, [disabled, authController, authenticator, getAppCheckToken, appCheckForceRefresh, dataSourceDelegate, storageSource]);
124
+
125
+ useEffect(() => {
126
+ checkAuthentication();
127
+ }, [checkAuthentication]);
128
+
129
+ return {
130
+ canAccessMainView,
131
+ authLoading: authenticationEnabled && authLoading,
132
+ notAllowedError,
133
+ authVerified
134
+ }
135
+ }