@firecms/core 3.0.0-beta.2 → 3.0.0-beta.4

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 (180) hide show
  1. package/README.md +1 -1
  2. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +3 -7
  3. package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
  4. package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +2 -2
  5. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +1 -2
  6. package/dist/components/EntityCollectionView/useSelectionController.d.ts +2 -0
  7. package/dist/components/EntityPreview.d.ts +25 -7
  8. package/dist/components/EntityView.d.ts +11 -0
  9. package/dist/components/FieldCaption.d.ts +5 -0
  10. package/dist/components/HomePage/NavigationCard.d.ts +8 -0
  11. package/dist/components/HomePage/{NavigationCollectionCard.d.ts → NavigationCardBinding.d.ts} +2 -2
  12. package/dist/components/HomePage/SmallNavigationCard.d.ts +6 -0
  13. package/dist/components/HomePage/index.d.ts +3 -1
  14. package/dist/components/ReferenceWidget.d.ts +3 -3
  15. package/dist/components/VirtualTable/VirtualTableProps.d.ts +1 -1
  16. package/dist/components/index.d.ts +4 -3
  17. package/dist/contexts/AuthControllerContext.d.ts +1 -1
  18. package/dist/{internal/EntityView.d.ts → core/EntityEditView.d.ts} +2 -2
  19. package/dist/core/SideEntityView.d.ts +7 -0
  20. package/dist/core/index.d.ts +0 -2
  21. package/dist/form/EntityForm.d.ts +1 -1
  22. package/dist/form/components/StorageItemPreview.d.ts +3 -2
  23. package/dist/form/components/StorageUploadProgress.d.ts +1 -1
  24. package/dist/form/components/index.d.ts +1 -0
  25. package/dist/form/field_bindings/KeyValueFieldBinding.d.ts +1 -1
  26. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  27. package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +4 -3
  28. package/dist/form/field_bindings/TextFieldBinding.d.ts +2 -2
  29. package/dist/form/index.d.ts +1 -0
  30. package/dist/form/validation.d.ts +1 -1
  31. package/dist/hooks/data/delete.d.ts +2 -2
  32. package/dist/hooks/data/save.d.ts +1 -1
  33. package/dist/hooks/data/useDataSource.d.ts +2 -2
  34. package/dist/hooks/data/useEntityFetch.d.ts +3 -3
  35. package/dist/hooks/index.d.ts +3 -1
  36. package/dist/{core → hooks}/useBuildModeController.d.ts +1 -1
  37. package/dist/hooks/useBuildNavigationController.d.ts +5 -2
  38. package/dist/hooks/useProjectLog.d.ts +6 -2
  39. package/dist/hooks/useStorageSource.d.ts +2 -2
  40. package/dist/hooks/useValidateAuthenticator.d.ts +25 -0
  41. package/dist/index.es.js +8575 -8207
  42. package/dist/index.es.js.map +1 -1
  43. package/dist/index.umd.js +5 -5
  44. package/dist/index.umd.js.map +1 -1
  45. package/dist/internal/useBuildDataSource.d.ts +4 -0
  46. package/dist/preview/PropertyPreview.d.ts +1 -1
  47. package/dist/preview/PropertyPreviewProps.d.ts +1 -4
  48. package/dist/preview/components/BooleanPreview.d.ts +5 -1
  49. package/dist/preview/components/EnumValuesChip.d.ts +1 -1
  50. package/dist/preview/components/ReferencePreview.d.ts +1 -7
  51. package/dist/types/analytics.d.ts +1 -1
  52. package/dist/types/auth.d.ts +37 -1
  53. package/dist/types/collections.d.ts +17 -4
  54. package/dist/types/datasource.d.ts +1 -1
  55. package/dist/types/entities.d.ts +1 -0
  56. package/dist/types/entity_callbacks.d.ts +2 -2
  57. package/dist/types/entity_overrides.d.ts +6 -0
  58. package/dist/types/index.d.ts +2 -0
  59. package/dist/types/navigation.d.ts +14 -13
  60. package/dist/types/permissions.d.ts +5 -1
  61. package/dist/types/plugins.d.ts +17 -19
  62. package/dist/types/properties.d.ts +2 -2
  63. package/dist/types/property_config.d.ts +2 -2
  64. package/dist/types/roles.d.ts +31 -0
  65. package/dist/types/storage.d.ts +11 -3
  66. package/dist/types/user.d.ts +5 -0
  67. package/dist/util/collections.d.ts +9 -1
  68. package/dist/util/icons.d.ts +8 -2
  69. package/dist/util/index.d.ts +1 -0
  70. package/dist/util/permissions.d.ts +4 -4
  71. package/dist/util/references.d.ts +4 -2
  72. package/dist/util/resolutions.d.ts +1 -1
  73. package/dist/util/useTraceUpdate.d.ts +1 -0
  74. package/package.json +31 -23
  75. package/src/components/DeleteEntityDialog.tsx +4 -4
  76. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +35 -39
  77. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +276 -282
  78. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +1 -1
  79. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +13 -13
  80. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +10 -17
  81. package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +3 -3
  82. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +1 -1
  83. package/src/components/EntityCollectionTable/internal/default_entity_actions.tsx +9 -5
  84. package/src/components/EntityCollectionView/EntityCollectionView.tsx +37 -58
  85. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +5 -6
  86. package/src/components/EntityCollectionView/useSelectionController.tsx +30 -0
  87. package/src/components/EntityPreview.tsx +207 -70
  88. package/src/components/EntityView.tsx +84 -0
  89. package/src/components/FieldCaption.tsx +14 -0
  90. package/src/components/FireCMSAppBar.tsx +8 -0
  91. package/src/components/HomePage/DefaultHomePage.tsx +14 -10
  92. package/src/components/HomePage/NavigationCard.tsx +69 -0
  93. package/src/components/HomePage/NavigationCardBinding.tsx +116 -0
  94. package/src/components/HomePage/SmallNavigationCard.tsx +45 -0
  95. package/src/components/HomePage/index.tsx +3 -1
  96. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -4
  97. package/src/components/ReferenceWidget.tsx +8 -8
  98. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +11 -19
  99. package/src/components/VirtualTable/VirtualTableProps.tsx +1 -1
  100. package/src/components/common/useDataSourceEntityCollectionTableController.tsx +1 -1
  101. package/src/components/index.tsx +4 -3
  102. package/src/contexts/AuthControllerContext.tsx +1 -1
  103. package/src/core/Drawer.tsx +66 -39
  104. package/src/{internal/EntityView.tsx → core/EntityEditView.tsx} +22 -39
  105. package/src/core/EntitySidePanel.tsx +2 -2
  106. package/src/core/FireCMS.tsx +18 -2
  107. package/src/core/NavigationRoutes.tsx +8 -0
  108. package/src/core/SideEntityView.tsx +38 -0
  109. package/src/core/index.tsx +0 -2
  110. package/src/form/EntityForm.tsx +20 -12
  111. package/src/form/components/StorageItemPreview.tsx +5 -3
  112. package/src/form/components/StorageUploadProgress.tsx +6 -5
  113. package/src/form/components/index.tsx +1 -0
  114. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -3
  115. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +12 -15
  116. package/src/form/field_bindings/BlockFieldBinding.tsx +2 -3
  117. package/src/form/field_bindings/DateTimeFieldBinding.tsx +3 -3
  118. package/src/form/field_bindings/KeyValueFieldBinding.tsx +18 -18
  119. package/src/form/field_bindings/MapFieldBinding.tsx +17 -17
  120. package/src/form/field_bindings/MarkdownFieldBinding.tsx +1 -2
  121. package/src/form/field_bindings/MultiSelectBinding.tsx +2 -3
  122. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -3
  123. package/src/form/field_bindings/ReferenceFieldBinding.tsx +6 -4
  124. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
  125. package/src/form/field_bindings/SelectFieldBinding.tsx +2 -3
  126. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +15 -6
  127. package/src/form/field_bindings/SwitchFieldBinding.tsx +2 -3
  128. package/src/form/field_bindings/TextFieldBinding.tsx +10 -9
  129. package/src/form/index.tsx +1 -0
  130. package/src/form/validation.ts +3 -4
  131. package/src/hooks/data/delete.ts +3 -3
  132. package/src/hooks/data/save.ts +1 -1
  133. package/src/hooks/data/useCollectionFetch.tsx +1 -1
  134. package/src/hooks/data/useDataSource.tsx +8 -3
  135. package/src/hooks/data/useEntityFetch.tsx +4 -4
  136. package/src/hooks/index.tsx +5 -1
  137. package/src/{core → hooks}/useBuildModeController.tsx +1 -1
  138. package/src/hooks/useBuildNavigationController.tsx +153 -76
  139. package/src/hooks/useProjectLog.tsx +16 -6
  140. package/src/hooks/useReferenceDialog.tsx +2 -2
  141. package/src/hooks/useStorageSource.tsx +7 -2
  142. package/src/hooks/useValidateAuthenticator.tsx +135 -0
  143. package/src/internal/useBuildDataSource.ts +7 -2
  144. package/src/internal/useBuildSideEntityController.tsx +3 -0
  145. package/src/preview/PropertyPreview.tsx +2 -2
  146. package/src/preview/PropertyPreviewProps.tsx +1 -11
  147. package/src/preview/components/BooleanPreview.tsx +19 -4
  148. package/src/preview/components/EnumValuesChip.tsx +1 -1
  149. package/src/preview/components/ReferencePreview.tsx +56 -148
  150. package/src/preview/property_previews/StringPropertyPreview.tsx +8 -7
  151. package/src/types/analytics.ts +1 -0
  152. package/src/types/auth.tsx +50 -1
  153. package/src/types/collections.ts +19 -4
  154. package/src/types/datasource.ts +1 -1
  155. package/src/types/entities.ts +4 -0
  156. package/src/types/entity_actions.tsx +4 -0
  157. package/src/types/entity_callbacks.ts +2 -2
  158. package/src/types/entity_overrides.tsx +7 -0
  159. package/src/types/firecms.tsx +0 -1
  160. package/src/types/index.ts +2 -0
  161. package/src/types/navigation.ts +17 -16
  162. package/src/types/permissions.ts +6 -1
  163. package/src/types/plugins.tsx +24 -27
  164. package/src/types/properties.ts +3 -2
  165. package/src/types/property_config.tsx +2 -2
  166. package/src/types/roles.ts +41 -0
  167. package/src/types/side_entity_controller.tsx +1 -0
  168. package/src/types/storage.ts +12 -3
  169. package/src/types/user.ts +7 -0
  170. package/src/util/collections.ts +22 -0
  171. package/src/util/icons.tsx +11 -3
  172. package/src/util/index.ts +1 -0
  173. package/src/util/permissions.ts +11 -8
  174. package/src/util/references.ts +36 -5
  175. package/src/util/useTraceUpdate.tsx +2 -1
  176. package/src/components/HomePage/NavigationCollectionCard.tsx +0 -146
  177. /package/dist/{components → form/components}/LabelWithIcon.d.ts +0 -0
  178. /package/dist/{core → hooks}/useBuildLocalConfigurationPersistence.d.ts +0 -0
  179. /package/src/{components → form/components}/LabelWithIcon.tsx +0 -0
  180. /package/src/{core → hooks}/useBuildLocalConfigurationPersistence.tsx +0 -0
@@ -1,5 +1,5 @@
1
- import { useCallback, useEffect, useMemo, useState } from "react";
2
- import { useLocation } from "react-router-dom";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import equal from "react-fast-compare"
3
3
 
4
4
  import {
5
5
  AuthController,
@@ -10,12 +10,14 @@ import {
10
10
  EntityCollectionsBuilder,
11
11
  EntityReference,
12
12
  NavigationController,
13
+ PermissionsBuilder,
13
14
  TopNavigationEntry,
14
15
  TopNavigationResult,
15
16
  User,
16
17
  UserConfigurationPersistence
17
18
  } from "../types";
18
19
  import {
20
+ applyPermissionsFunctionIfEmpty,
19
21
  getCollectionByPathOrId,
20
22
  mergeDeep,
21
23
  removeInitialAndTrailingSlashes,
@@ -32,7 +34,10 @@ type BuildNavigationContextProps<EC extends EntityCollection, UserType extends U
32
34
  baseCollectionPath?: string,
33
35
  authController: AuthController<UserType>;
34
36
  collections?: EC[] | EntityCollectionsBuilder<EC>;
37
+ collectionPermissions?: PermissionsBuilder;
35
38
  views?: CMSView[] | CMSViewsBuilder;
39
+ adminViews?: CMSView[] | CMSViewsBuilder;
40
+ viewsOrder?: string[];
36
41
  userConfigPersistence?: UserConfigurationPersistence;
37
42
  dataSourceDelegate: DataSourceDelegate;
38
43
  /**
@@ -45,21 +50,25 @@ type BuildNavigationContextProps<EC extends EntityCollection, UserType extends U
45
50
  injectCollections?: (collections: EntityCollection[]) => EntityCollection[];
46
51
  };
47
52
 
48
- export function useBuildNavigationController<EC extends EntityCollection, UserType extends User>({
49
- basePath = DEFAULT_BASE_PATH,
50
- baseCollectionPath = DEFAULT_COLLECTION_PATH,
51
- authController,
52
- collections: baseCollections,
53
- views: baseViews,
54
- userConfigPersistence,
55
- dataSourceDelegate,
56
- injectCollections
57
- }: BuildNavigationContextProps<EC, UserType>): NavigationController {
58
-
59
- const location = useLocation();
60
-
61
- const [collections, setCollections] = useState<EntityCollection[] | undefined>();
62
- 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
+
63
72
  const [initialised, setInitialised] = useState<boolean>(false);
64
73
 
65
74
  const [topLevelNavigation, setTopLevelNavigation] = useState<TopNavigationResult | undefined>(undefined);
@@ -79,19 +88,18 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
79
88
  const buildUrlCollectionPath = useCallback((path: string): string => `${removeInitialAndTrailingSlashes(baseCollectionPath)}/${encodePath(path)}`,
80
89
  [baseCollectionPath]);
81
90
 
82
- const computeTopNavigation = useCallback((collections: EntityCollection[], views: CMSView[]): TopNavigationResult => {
83
- // return (collection.editable && resolvePermissions(collection, authController, paths).editCollection) ?? DEFAULT_PERMISSIONS.editCollection;
84
- const navigationEntries: TopNavigationEntry[] = [
91
+ const computeTopNavigation = useCallback((collections: EntityCollection[], views: CMSView[], adminViews: CMSView[], viewsOrder?: string[]): TopNavigationResult => {
92
+ let navigationEntries: TopNavigationEntry[] = [
85
93
  ...(collections ?? []).map(collection => (!collection.hideFromNavigation
86
- ? {
94
+ ? ({
87
95
  url: buildUrlCollectionPath(collection.id ?? collection.path),
88
96
  type: "collection",
89
97
  name: collection.name.trim(),
90
98
  path: collection.id ?? collection.path,
91
99
  collection,
92
100
  description: collection.description?.trim(),
93
- group: collection.group?.trim()
94
- }
101
+ group: getGroup(collection)
102
+ } satisfies TopNavigationEntry)
95
103
  : undefined))
96
104
  .filter(Boolean) as TopNavigationEntry[],
97
105
  ...(views ?? []).map(view =>
@@ -100,18 +108,50 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
100
108
  url: buildCMSUrlPath(Array.isArray(view.path) ? view.path[0] : view.path),
101
109
  name: view.name.trim(),
102
110
  type: "view",
111
+ path: view.path,
103
112
  view,
104
113
  description: view.description?.trim(),
105
- group: view.group?.trim()
106
- })
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)
107
129
  : undefined)
108
130
  .filter(Boolean) as TopNavigationEntry[]
109
131
  ];
110
132
 
133
+ if (viewsOrder) {
134
+ navigationEntries = navigationEntries.sort((a, b) => {
135
+ const aIndex = viewsOrder.indexOf(a.path);
136
+ const bIndex = viewsOrder.indexOf(b.path);
137
+ if (aIndex === -1 && bIndex === -1) {
138
+ return 0;
139
+ }
140
+ if (aIndex === -1) {
141
+ return 1;
142
+ }
143
+ if (bIndex === -1) {
144
+ return -1;
145
+ }
146
+ return aIndex - bIndex;
147
+ });
148
+ }
149
+
111
150
  const groups: string[] = Object.values(navigationEntries)
112
151
  .map(e => e.group)
113
152
  .filter(Boolean)
114
153
  .filter((value, index, array) => array.indexOf(value) === index) as string[];
154
+
115
155
  return {
116
156
  navigationEntries,
117
157
  groups
@@ -124,15 +164,25 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
124
164
  return;
125
165
 
126
166
  try {
127
- const [resolvedCollections = [], resolvedViews = []] = await Promise.all([
128
- resolveCollections(baseCollections, authController, dataSourceDelegate, injectCollections),
129
- resolveCMSViews(baseViews, authController, dataSourceDelegate)
167
+
168
+ const [resolvedCollections = [], resolvedViews, resolvedAdminViews = []] = await Promise.all([
169
+ resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate, injectCollections),
170
+ resolveCMSViews(viewsProp, authController, dataSourceDelegate),
171
+ resolveCMSViews(adminViewsProp, authController, dataSourceDelegate)
130
172
  ]
131
173
  );
132
174
 
133
- setCollections(resolvedCollections);
134
- setViews(resolvedViews);
135
- setTopLevelNavigation(computeTopNavigation(resolvedCollections ?? [], resolvedViews));
175
+ if (
176
+ !equal(collectionsRef.current, resolvedCollections) ||
177
+ !equal(viewsRef.current, resolvedViews) ||
178
+ !equal(adminViewsRef.current, resolvedAdminViews) ||
179
+ !equal(topLevelNavigation, computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder))
180
+ ) {
181
+ collectionsRef.current = resolvedCollections;
182
+ viewsRef.current = resolvedViews;
183
+ adminViewsRef.current = resolvedAdminViews;
184
+ setTopLevelNavigation(computeTopNavigation(resolvedCollections ?? [], resolvedViews, resolvedAdminViews, viewsOrder));
185
+ }
136
186
  } catch (e) {
137
187
  console.error(e);
138
188
  setNavigationLoadingError(e as any);
@@ -140,18 +190,28 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
140
190
 
141
191
  setNavigationLoading(false);
142
192
  setInitialised(true);
143
- }, [baseCollections, authController.user, authController.initialLoading, baseViews, computeTopNavigation, injectCollections]);
193
+
194
+ }, [
195
+ collectionsProp,
196
+ collectionPermissions,
197
+ authController.user,
198
+ authController.initialLoading,
199
+ viewsProp,
200
+ adminViewsProp,
201
+ computeTopNavigation,
202
+ injectCollections
203
+ ]);
144
204
 
145
205
  useEffect(() => {
146
206
  refreshNavigation();
147
207
  }, [refreshNavigation]);
148
208
 
149
- const getCollection = useCallback(<EC extends EntityCollection>(
209
+ const getCollection = useCallback((
150
210
  idOrPath: string,
151
211
  entityId?: string,
152
212
  includeUserOverride = false
153
213
  ): EC | undefined => {
154
-
214
+ const collections = collectionsRef.current;
155
215
  if (!collections)
156
216
  return undefined;
157
217
 
@@ -179,14 +239,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
179
239
 
180
240
  return { ...overriddenCollection, ...result } as EC;
181
241
 
182
- }, [
183
- basePath,
184
- baseCollectionPath,
185
- collections,
186
- ]);
242
+ }, [userConfigPersistence]);
187
243
 
188
244
  const getCollectionFromPaths = useCallback(<EC extends EntityCollection>(pathSegments: string[]): EC | undefined => {
189
- let currentCollections = collections;
245
+
246
+ const collections = collectionsRef.current;
247
+ let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
190
248
  if (!currentCollections)
191
249
  throw Error("Collections have not been initialised yet");
192
250
 
@@ -202,10 +260,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
202
260
 
203
261
  return undefined;
204
262
 
205
- }, [collections]);
263
+ }, []);
206
264
 
207
265
  const getCollectionFromIds = useCallback(<EC extends EntityCollection>(ids: string[]): EC | undefined => {
208
- let currentCollections = collections;
266
+
267
+ const collections = collectionsRef.current;
268
+ let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
209
269
  if (!currentCollections)
210
270
  throw Error("Collections have not been initialised yet");
211
271
 
@@ -221,7 +281,7 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
221
281
 
222
282
  return undefined;
223
283
 
224
- }, [collections]);
284
+ }, []);
225
285
 
226
286
  const isUrlCollectionPath = useCallback(
227
287
  (path: string): boolean => removeInitialAndTrailingSlashes(path + "/").startsWith(removeInitialAndTrailingSlashes(fullCollectionPath) + "/"),
@@ -243,48 +303,55 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
243
303
  []);
244
304
 
245
305
  const resolveAliasesFrom = useCallback((path: string): string => {
306
+ const collections = collectionsRef.current;
246
307
  if (!collections)
247
308
  throw Error("Collections have not been initialised yet");
248
309
  return resolveCollectionPathIds(path, collections);
249
- }, [collections]);
250
-
251
- const state = location.state as any;
252
- /**
253
- * The location can be overridden if `base_location` is set in the
254
- * state field of the current location. This can happen if you open
255
- * a side entity, like `products`, from a different one, like `users`
256
- */
257
- const baseLocation = state && state.base_location ? state.base_location : location;
310
+ }, []);
258
311
 
259
- const getAllParentCollectionsForPath = useCallback((path: string): EntityReference[] => {
312
+ const getAllParentReferencesForPath = useCallback((path: string): EntityReference[] => {
313
+ const collections = collectionsRef.current ?? [];
260
314
  return getParentReferencesFromPath({
261
315
  path,
262
316
  collections
263
317
  });
264
- }, [collections]);
318
+ }, []);
265
319
 
266
320
  const getParentCollectionIds = useCallback((path: string): string[] => {
267
- return getAllParentCollectionsForPath(path).map(r => r.id);
268
- }, [getAllParentCollectionsForPath])
321
+
322
+ const strings = path.split("/");
323
+ const oddPathSegments = strings.filter((_, i) => i % 2 === 0);
324
+ oddPathSegments.pop();
325
+
326
+ const result: string[][] = [];
327
+
328
+ for (let i = 1; i <= oddPathSegments.length; i++) {
329
+ result.push(oddPathSegments.slice(0, i));
330
+ }
331
+
332
+ // for each odd path segment, get the collection
333
+ return result.map(r => getCollectionFromPaths(r)?.id).filter(Boolean) as string[];
334
+ }, [getAllParentReferencesForPath])
269
335
 
270
336
  const convertIdsToPaths = useCallback((ids: string[]): string[] => {
271
- let currentCollections = collections;
272
- const paths: string[] = [];
273
- for (let i = 0; i < ids.length; i++) {
274
- const id = ids[i];
275
- const collection: EntityCollection | undefined = currentCollections!.find(c => c.id === id);
276
- if (!collection)
277
- throw Error(`Collection with id ${id} not found`);
278
- paths.push(collection.path);
279
- currentCollections = collection.subcollections;
280
- }
281
- return paths;
337
+ const collections = collectionsRef.current;
338
+ let currentCollections = collections;
339
+ const paths: string[] = [];
340
+ for (let i = 0; i < ids.length; i++) {
341
+ const id = ids[i];
342
+ const collection: EntityCollection | undefined = currentCollections!.find(c => c.id === id);
343
+ if (!collection)
344
+ throw Error(`Collection with id ${id} not found`);
345
+ paths.push(collection.path);
346
+ currentCollections = collection.subcollections;
282
347
  }
283
- , [getCollectionFromIds]);
348
+ return paths;
349
+ }, [getCollectionFromIds]);
284
350
 
285
- return useMemo(() => ({
286
- collections,
287
- views,
351
+ return {
352
+ collections: collectionsRef.current,
353
+ views: viewsRef.current,
354
+ adminViews: adminViewsRef.current,
288
355
  loading: !initialised || navigationLoading,
289
356
  navigationLoadingError,
290
357
  homeUrl,
@@ -301,12 +368,11 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
301
368
  buildCMSUrlPath,
302
369
  resolveAliasesFrom,
303
370
  topLevelNavigation,
304
- baseLocation,
305
371
  refreshNavigation,
306
- getParentReferencesFromPath: getAllParentCollectionsForPath,
372
+ getParentReferencesFromPath: getAllParentReferencesForPath,
307
373
  getParentCollectionIds,
308
374
  convertIdsToPaths
309
- }), [baseCollectionPath, baseLocation, basePath, buildCMSUrlPath, buildUrlCollectionPath, buildUrlEditCollectionPath, collections, getAllParentCollectionsForPath, getCollection, getCollectionFromPaths, homeUrl, initialised, isUrlCollectionPath, navigationLoading, navigationLoadingError, refreshNavigation, resolveAliasesFrom, topLevelNavigation, urlPathToDataPath, views]);
375
+ };
310
376
  }
311
377
 
312
378
  export function getSidePanelKey(path: string, entityId?: string) {
@@ -326,8 +392,8 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
326
392
  return resolvedCollections
327
393
  .filter((c) => {
328
394
  if (!c.permissions) return true;
329
- const resolvedPermissions = resolvePermissions(c, authController, [c.path], null,)
330
- return resolvedPermissions.read !== false;
395
+ const resolvedPermissions = resolvePermissions(c, authController, c.path, null)
396
+ return resolvedPermissions?.read !== false;
331
397
  })
332
398
  .map((c) => {
333
399
  if (!c.subcollections) return c;
@@ -339,6 +405,7 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
339
405
  }
340
406
 
341
407
  async function resolveCollections(collections: undefined | EntityCollection[] | EntityCollectionsBuilder<any>,
408
+ collectionPermissions: PermissionsBuilder | undefined,
342
409
  authController: AuthController,
343
410
  dataSource: DataSourceDelegate,
344
411
  injectCollections?: (collections: EntityCollection[]) => EntityCollection[]) {
@@ -353,6 +420,8 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
353
420
  resolvedCollections = collections;
354
421
  }
355
422
 
423
+ resolvedCollections = applyPermissionsFunctionIfEmpty(resolvedCollections, collectionPermissions);
424
+
356
425
  resolvedCollections = filterOutNotAllowedCollections(resolvedCollections, authController);
357
426
 
358
427
  if (injectCollections) {
@@ -375,3 +444,11 @@ async function resolveCMSViews(baseViews: CMSView[] | CMSViewsBuilder | undefine
375
444
  }
376
445
  return resolvedViews;
377
446
  }
447
+
448
+ function getGroup(collectionOrView: EntityCollection<any, any> | CMSView) {
449
+ const trimmed = collectionOrView.group?.trim();
450
+ if (!trimmed || trimmed === "") {
451
+ return "Views";
452
+ }
453
+ return trimmed ?? "Views";
454
+ }
@@ -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
+ }
@@ -330,6 +330,10 @@ export function useBuildDataSource({
330
330
  * bindings.
331
331
  * Also, Firestore references are replaced with {@link EntityReference}
332
332
  * @param data
333
+ * @param buildReference
334
+ * @param buildGeoPoint
335
+ * @param buildDate
336
+ * @param buildDelete
333
337
  * @group Firestore
334
338
  */
335
339
  export function cmsToDelegateModel(data: any,
@@ -340,9 +344,11 @@ export function cmsToDelegateModel(data: any,
340
344
  ): any {
341
345
  if (data === undefined) {
342
346
  return buildDelete();
347
+ } else if (data === null) {
348
+ return null;
343
349
  } else if (Array.isArray(data)) {
344
350
  return data.map(v => cmsToDelegateModel(v, buildReference, buildGeoPoint, buildDate, buildDelete));
345
- } else if (data instanceof EntityReference) {
351
+ } else if (data.isEntityReference && data.isEntityReference()) {
346
352
  return buildReference(data);
347
353
  } else if (data instanceof GeoPoint) {
348
354
  return buildGeoPoint(data);
@@ -355,4 +361,3 @@ export function cmsToDelegateModel(data: any,
355
361
  }
356
362
  return data;
357
363
  }
358
-
@@ -34,6 +34,7 @@ export const useBuildSideEntityController = (navigation: NavigationController,
34
34
  // only on initialisation, create panels from URL
35
35
  useEffect(() => {
36
36
  if (!navigation.loading && !initialised.current) {
37
+ console.debug("Initialising side entity controller");
37
38
  if (navigation.isUrlCollectionPath(location.pathname)) {
38
39
  const newFlag = location.hash === `#${NEW_URL_HASH}`;
39
40
  const entityOrCollectionPath = navigation.urlPathToDataPath(location.pathname);
@@ -47,6 +48,8 @@ export const useBuildSideEntityController = (navigation: NavigationController,
47
48
  sideDialogsController.open(propsToSidePanel(panel, navigation, smallLayout))
48
49
  }, 1);
49
50
  }
51
+ } else {
52
+ console.warn("Location path is not a collection path");
50
53
  }
51
54
  initialised.current = true;
52
55
  }