@firecms/core 3.0.0-canary.3 → 3.0.0-canary.31

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 +2 -2
  2. package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +1 -1
  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 +6 -4
  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 +8056 -7704
  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/permissions.d.ts +4 -4
  70. package/dist/util/references.d.ts +4 -2
  71. package/dist/util/resolutions.d.ts +1 -1
  72. package/dist/util/useTraceUpdate.d.ts +1 -0
  73. package/package.json +24 -24
  74. package/src/components/DeleteEntityDialog.tsx +4 -4
  75. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  76. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +273 -277
  77. package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +1 -1
  78. package/src/components/EntityCollectionTable/PropertyTableCell.tsx +13 -13
  79. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +10 -17
  80. package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +3 -3
  81. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +1 -1
  82. package/src/components/EntityCollectionTable/internal/default_entity_actions.tsx +9 -5
  83. package/src/components/EntityCollectionView/EntityCollectionView.tsx +28 -49
  84. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +5 -6
  85. package/src/components/EntityCollectionView/useSelectionController.tsx +30 -0
  86. package/src/components/EntityPreview.tsx +207 -70
  87. package/src/components/EntityView.tsx +84 -0
  88. package/src/components/FieldCaption.tsx +14 -0
  89. package/src/components/FireCMSAppBar.tsx +8 -0
  90. package/src/components/HomePage/DefaultHomePage.tsx +14 -10
  91. package/src/components/HomePage/NavigationCard.tsx +69 -0
  92. package/src/components/HomePage/NavigationCardBinding.tsx +116 -0
  93. package/src/components/HomePage/SmallNavigationCard.tsx +45 -0
  94. package/src/components/HomePage/index.tsx +3 -1
  95. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -4
  96. package/src/components/ReferenceWidget.tsx +8 -8
  97. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +1 -1
  98. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +11 -19
  99. package/src/components/VirtualTable/VirtualTableProps.tsx +1 -1
  100. package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +1 -1
  101. package/src/components/common/useDataSourceEntityCollectionTableController.tsx +1 -1
  102. package/src/components/index.tsx +4 -3
  103. package/src/contexts/AuthControllerContext.tsx +1 -1
  104. package/src/core/Drawer.tsx +66 -39
  105. package/src/{internal/EntityView.tsx → core/EntityEditView.tsx} +22 -39
  106. package/src/core/EntitySidePanel.tsx +2 -2
  107. package/src/core/FireCMS.tsx +18 -2
  108. package/src/core/NavigationRoutes.tsx +8 -0
  109. package/src/core/SideEntityView.tsx +38 -0
  110. package/src/core/index.tsx +0 -2
  111. package/src/form/EntityForm.tsx +20 -12
  112. package/src/form/components/StorageItemPreview.tsx +5 -3
  113. package/src/form/components/StorageUploadProgress.tsx +6 -5
  114. package/src/form/components/index.tsx +1 -0
  115. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -3
  116. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +12 -15
  117. package/src/form/field_bindings/BlockFieldBinding.tsx +2 -3
  118. package/src/form/field_bindings/DateTimeFieldBinding.tsx +4 -4
  119. package/src/form/field_bindings/KeyValueFieldBinding.tsx +18 -18
  120. package/src/form/field_bindings/MapFieldBinding.tsx +17 -17
  121. package/src/form/field_bindings/MarkdownFieldBinding.tsx +1 -2
  122. package/src/form/field_bindings/MultiSelectBinding.tsx +2 -3
  123. package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -3
  124. package/src/form/field_bindings/ReferenceFieldBinding.tsx +6 -4
  125. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
  126. package/src/form/field_bindings/SelectFieldBinding.tsx +2 -3
  127. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +15 -6
  128. package/src/form/field_bindings/SwitchFieldBinding.tsx +2 -3
  129. package/src/form/field_bindings/TextFieldBinding.tsx +10 -9
  130. package/src/form/index.tsx +1 -0
  131. package/src/form/validation.ts +3 -4
  132. package/src/hooks/data/delete.ts +3 -3
  133. package/src/hooks/data/save.ts +1 -1
  134. package/src/hooks/data/useCollectionFetch.tsx +1 -1
  135. package/src/hooks/data/useDataSource.tsx +8 -3
  136. package/src/hooks/data/useEntityFetch.tsx +4 -4
  137. package/src/hooks/index.tsx +5 -1
  138. package/src/{core → hooks}/useBuildModeController.tsx +1 -1
  139. package/src/hooks/useBuildNavigationController.tsx +133 -72
  140. package/src/hooks/useProjectLog.tsx +16 -6
  141. package/src/hooks/useReferenceDialog.tsx +2 -2
  142. package/src/hooks/useStorageSource.tsx +7 -2
  143. package/src/hooks/useValidateAuthenticator.tsx +135 -0
  144. package/src/internal/useBuildDataSource.ts +7 -2
  145. package/src/internal/useBuildSideEntityController.tsx +3 -0
  146. package/src/preview/PropertyPreview.tsx +2 -2
  147. package/src/preview/PropertyPreviewProps.tsx +1 -11
  148. package/src/preview/components/BooleanPreview.tsx +19 -4
  149. package/src/preview/components/EnumValuesChip.tsx +1 -1
  150. package/src/preview/components/ReferencePreview.tsx +56 -148
  151. package/src/preview/property_previews/StringPropertyPreview.tsx +8 -7
  152. package/src/types/analytics.ts +1 -0
  153. package/src/types/auth.tsx +50 -1
  154. package/src/types/collections.ts +19 -4
  155. package/src/types/datasource.ts +1 -1
  156. package/src/types/entities.ts +4 -0
  157. package/src/types/entity_actions.tsx +4 -0
  158. package/src/types/entity_callbacks.ts +2 -2
  159. package/src/types/entity_overrides.tsx +7 -0
  160. package/src/types/firecms.tsx +0 -1
  161. package/src/types/index.ts +2 -0
  162. package/src/types/navigation.ts +17 -16
  163. package/src/types/permissions.ts +6 -1
  164. package/src/types/plugins.tsx +24 -27
  165. package/src/types/properties.ts +3 -2
  166. package/src/types/property_config.tsx +2 -2
  167. package/src/types/roles.ts +41 -0
  168. package/src/types/side_entity_controller.tsx +1 -0
  169. package/src/types/storage.ts +12 -3
  170. package/src/types/user.ts +7 -0
  171. package/src/util/collections.ts +22 -0
  172. package/src/util/icons.tsx +11 -3
  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,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,50 @@ 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,
112
+ view,
113
+ description: view.description?.trim(),
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,
104
125
  view,
105
126
  description: view.description?.trim(),
106
- group: view.group?.trim()
107
- })
127
+ group: "Admin"
128
+ } satisfies TopNavigationEntry)
108
129
  : undefined)
109
130
  .filter(Boolean) as TopNavigationEntry[]
110
131
  ];
111
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
+
112
150
  const groups: string[] = Object.values(navigationEntries)
113
151
  .map(e => e.group)
114
152
  .filter(Boolean)
115
153
  .filter((value, index, array) => array.indexOf(value) === index) as string[];
154
+
116
155
  return {
117
156
  navigationEntries,
118
157
  groups
@@ -125,16 +164,24 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
125
164
  return;
126
165
 
127
166
  try {
128
- const [resolvedCollections = [], resolvedViews = []] = await Promise.all([
129
- resolveCollections(collectionsProp, authController, dataSourceDelegate, injectCollections),
130
- 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)
131
172
  ]
132
173
  );
133
- if (!equal(collectionsRef.current, resolvedCollections) || !equal(views, resolvedViews) || !equal(topLevelNavigation, computeTopNavigation(resolvedCollections, resolvedViews))) {
174
+
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
+ ) {
134
181
  collectionsRef.current = resolvedCollections;
135
- setCollections(resolvedCollections);
136
- setViews(resolvedViews);
137
- setTopLevelNavigation(computeTopNavigation(resolvedCollections ?? [], resolvedViews));
182
+ viewsRef.current = resolvedViews;
183
+ adminViewsRef.current = resolvedAdminViews;
184
+ setTopLevelNavigation(computeTopNavigation(resolvedCollections ?? [], resolvedViews, resolvedAdminViews, viewsOrder));
138
185
  }
139
186
  } catch (e) {
140
187
  console.error(e);
@@ -143,25 +190,34 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
143
190
 
144
191
  setNavigationLoading(false);
145
192
  setInitialised(true);
146
- }, [collectionsProp, 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
+ ]);
147
204
 
148
205
  useEffect(() => {
149
206
  refreshNavigation();
150
207
  }, [refreshNavigation]);
151
208
 
152
- const getCollection = useCallback(<EC extends EntityCollection>(
209
+ const getCollection = useCallback((
153
210
  idOrPath: string,
154
211
  entityId?: string,
155
212
  includeUserOverride = false
156
213
  ): EC | undefined => {
157
-
214
+ const collections = collectionsRef.current;
158
215
  if (!collections)
159
216
  return undefined;
160
217
 
161
218
  const baseCollection = getCollectionByPathOrId(removeInitialAndTrailingSlashes(idOrPath), collections);
162
219
 
163
220
  const userOverride = includeUserOverride ? userConfigPersistence?.getCollectionConfig(idOrPath) : undefined;
164
-
165
221
  const overriddenCollection = baseCollection ? mergeDeep(baseCollection, userOverride) : undefined;
166
222
 
167
223
  let result: Partial<EntityCollection> | undefined = overriddenCollection;
@@ -182,14 +238,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
182
238
 
183
239
  return { ...overriddenCollection, ...result } as EC;
184
240
 
185
- }, [
186
- basePath,
187
- baseCollectionPath,
188
- collections,
189
- ]);
241
+ }, [userConfigPersistence]);
190
242
 
191
243
  const getCollectionFromPaths = useCallback(<EC extends EntityCollection>(pathSegments: string[]): EC | undefined => {
192
- let currentCollections = collections;
244
+
245
+ const collections = collectionsRef.current;
246
+ let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
193
247
  if (!currentCollections)
194
248
  throw Error("Collections have not been initialised yet");
195
249
 
@@ -205,10 +259,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
205
259
 
206
260
  return undefined;
207
261
 
208
- }, [collections]);
262
+ }, []);
209
263
 
210
264
  const getCollectionFromIds = useCallback(<EC extends EntityCollection>(ids: string[]): EC | undefined => {
211
- let currentCollections = collections;
265
+
266
+ const collections = collectionsRef.current;
267
+ let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
212
268
  if (!currentCollections)
213
269
  throw Error("Collections have not been initialised yet");
214
270
 
@@ -224,7 +280,7 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
224
280
 
225
281
  return undefined;
226
282
 
227
- }, [collections]);
283
+ }, []);
228
284
 
229
285
  const isUrlCollectionPath = useCallback(
230
286
  (path: string): boolean => removeInitialAndTrailingSlashes(path + "/").startsWith(removeInitialAndTrailingSlashes(fullCollectionPath) + "/"),
@@ -246,25 +302,19 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
246
302
  []);
247
303
 
248
304
  const resolveAliasesFrom = useCallback((path: string): string => {
305
+ const collections = collectionsRef.current;
249
306
  if (!collections)
250
307
  throw Error("Collections have not been initialised yet");
251
308
  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;
309
+ }, []);
261
310
 
262
311
  const getAllParentReferencesForPath = useCallback((path: string): EntityReference[] => {
312
+ const collections = collectionsRef.current ?? [];
263
313
  return getParentReferencesFromPath({
264
314
  path,
265
315
  collections
266
316
  });
267
- }, [collections]);
317
+ }, []);
268
318
 
269
319
  const getParentCollectionIds = useCallback((path: string): string[] => {
270
320
 
@@ -283,23 +333,24 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
283
333
  }, [getAllParentReferencesForPath])
284
334
 
285
335
  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;
336
+ const collections = collectionsRef.current;
337
+ let currentCollections = collections;
338
+ const paths: string[] = [];
339
+ for (let i = 0; i < ids.length; i++) {
340
+ const id = ids[i];
341
+ const collection: EntityCollection | undefined = currentCollections!.find(c => c.id === id);
342
+ if (!collection)
343
+ throw Error(`Collection with id ${id} not found`);
344
+ paths.push(collection.path);
345
+ currentCollections = collection.subcollections;
297
346
  }
298
- , [getCollectionFromIds]);
347
+ return paths;
348
+ }, [getCollectionFromIds]);
299
349
 
300
350
  return {
301
- collections,
302
- views,
351
+ collections: collectionsRef.current,
352
+ views: viewsRef.current,
353
+ adminViews: adminViewsRef.current,
303
354
  loading: !initialised || navigationLoading,
304
355
  navigationLoadingError,
305
356
  homeUrl,
@@ -316,7 +367,6 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
316
367
  buildCMSUrlPath,
317
368
  resolveAliasesFrom,
318
369
  topLevelNavigation,
319
- baseLocation,
320
370
  refreshNavigation,
321
371
  getParentReferencesFromPath: getAllParentReferencesForPath,
322
372
  getParentCollectionIds,
@@ -341,8 +391,8 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
341
391
  return resolvedCollections
342
392
  .filter((c) => {
343
393
  if (!c.permissions) return true;
344
- const resolvedPermissions = resolvePermissions(c, authController, [c.path], null,)
345
- return resolvedPermissions.read !== false;
394
+ const resolvedPermissions = resolvePermissions(c, authController, c.path, null)
395
+ return resolvedPermissions?.read !== false;
346
396
  })
347
397
  .map((c) => {
348
398
  if (!c.subcollections) return c;
@@ -354,6 +404,7 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
354
404
  }
355
405
 
356
406
  async function resolveCollections(collections: undefined | EntityCollection[] | EntityCollectionsBuilder<any>,
407
+ collectionPermissions: PermissionsBuilder | undefined,
357
408
  authController: AuthController,
358
409
  dataSource: DataSourceDelegate,
359
410
  injectCollections?: (collections: EntityCollection[]) => EntityCollection[]) {
@@ -368,6 +419,8 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
368
419
  resolvedCollections = collections;
369
420
  }
370
421
 
422
+ resolvedCollections = applyPermissionsFunctionIfEmpty(resolvedCollections, collectionPermissions);
423
+
371
424
  resolvedCollections = filterOutNotAllowedCollections(resolvedCollections, authController);
372
425
 
373
426
  if (injectCollections) {
@@ -390,3 +443,11 @@ async function resolveCMSViews(baseViews: CMSView[] | CMSViewsBuilder | undefine
390
443
  }
391
444
  return resolvedViews;
392
445
  }
446
+
447
+ function getGroup(collectionOrView: EntityCollection<any, any> | CMSView) {
448
+ const trimmed = collectionOrView.group?.trim();
449
+ if (!trimmed || trimmed === "") {
450
+ return "Views";
451
+ }
452
+ return trimmed ?? "Views";
453
+ }
@@ -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
  }
@@ -178,7 +178,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<T extends CMS
178
178
  }
179
179
  } else if (property.dataType === "reference") {
180
180
  if (typeof property.path === "string") {
181
- if (value instanceof EntityReference) {
181
+ if (typeof value === "object" && "isEntityReference" in value && value.isEntityReference()) {
182
182
  content = <ReferencePreview
183
183
  disabled={!property.path}
184
184
  previewProperties={property.previewProperties}
@@ -195,7 +195,7 @@ export const PropertyPreview = React.memo(function PropertyPreview<T extends CMS
195
195
 
196
196
  } else if (property.dataType === "boolean") {
197
197
  if (typeof value === "boolean") {
198
- content = <BooleanPreview value={value}/>;
198
+ content = <BooleanPreview value={value} size={size} property={property}/>;
199
199
  } else {
200
200
  content = buildWrongValueType(propertyKey, property.dataType, value);
201
201
  }