@firecms/core 3.0.0-canary.2 → 3.0.0-canary.21
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.
- package/README.md +1 -1
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +1 -1
- package/dist/components/EntityCollectionTable/EntityCollectionTable.d.ts +2 -2
- package/dist/components/EntityCollectionTable/PropertyTableCell.d.ts +2 -2
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +1 -2
- package/dist/components/EntityCollectionView/useSelectionController.d.ts +2 -0
- package/dist/components/EntityPreview.d.ts +25 -7
- package/dist/components/EntityView.d.ts +11 -0
- package/dist/components/FieldCaption.d.ts +5 -0
- package/dist/components/HomePage/NavigationCard.d.ts +8 -0
- package/dist/components/HomePage/{NavigationCollectionCard.d.ts → NavigationCardBinding.d.ts} +2 -2
- package/dist/components/HomePage/SmallNavigationCard.d.ts +6 -0
- package/dist/components/HomePage/index.d.ts +3 -1
- package/dist/components/ReferenceWidget.d.ts +3 -3
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +1 -1
- package/dist/components/index.d.ts +4 -3
- package/dist/contexts/AuthControllerContext.d.ts +1 -1
- package/dist/{internal/EntityView.d.ts → core/EntityEditView.d.ts} +2 -2
- package/dist/core/SideEntityView.d.ts +7 -0
- package/dist/core/index.d.ts +0 -2
- package/dist/form/EntityForm.d.ts +1 -1
- package/dist/form/components/StorageItemPreview.d.ts +3 -2
- package/dist/form/components/StorageUploadProgress.d.ts +1 -1
- package/dist/form/components/index.d.ts +1 -0
- package/dist/form/field_bindings/KeyValueFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/StorageUploadFieldBinding.d.ts +4 -3
- package/dist/form/field_bindings/TextFieldBinding.d.ts +2 -2
- package/dist/form/index.d.ts +1 -0
- package/dist/form/validation.d.ts +1 -1
- package/dist/hooks/data/delete.d.ts +2 -2
- package/dist/hooks/data/save.d.ts +1 -1
- package/dist/hooks/data/useDataSource.d.ts +2 -2
- package/dist/hooks/data/useEntityFetch.d.ts +3 -3
- package/dist/hooks/index.d.ts +3 -1
- package/dist/{core → hooks}/useBuildModeController.d.ts +1 -1
- package/dist/hooks/useBuildNavigationController.d.ts +5 -2
- package/dist/hooks/useProjectLog.d.ts +6 -2
- package/dist/hooks/useStorageSource.d.ts +2 -2
- package/dist/hooks/useValidateAuthenticator.d.ts +25 -0
- package/dist/index.es.js +8020 -7667
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +5 -5
- package/dist/index.umd.js.map +1 -1
- package/dist/internal/useBuildDataSource.d.ts +4 -0
- package/dist/preview/PropertyPreview.d.ts +1 -1
- package/dist/preview/PropertyPreviewProps.d.ts +1 -4
- package/dist/preview/components/BooleanPreview.d.ts +5 -1
- package/dist/preview/components/EnumValuesChip.d.ts +1 -1
- package/dist/preview/components/ReferencePreview.d.ts +1 -7
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/auth.d.ts +37 -1
- package/dist/types/collections.d.ts +17 -4
- package/dist/types/datasource.d.ts +1 -1
- package/dist/types/entities.d.ts +1 -0
- package/dist/types/entity_callbacks.d.ts +2 -2
- package/dist/types/entity_overrides.d.ts +6 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/navigation.d.ts +14 -13
- package/dist/types/permissions.d.ts +5 -1
- package/dist/types/plugins.d.ts +17 -19
- package/dist/types/properties.d.ts +2 -2
- package/dist/types/property_config.d.ts +2 -2
- package/dist/types/roles.d.ts +31 -0
- package/dist/types/user.d.ts +5 -0
- package/dist/util/collections.d.ts +9 -1
- package/dist/util/icons.d.ts +8 -2
- package/dist/util/permissions.d.ts +4 -4
- package/dist/util/references.d.ts +4 -2
- package/dist/util/resolutions.d.ts +1 -1
- package/dist/util/useTraceUpdate.d.ts +1 -0
- package/package.json +24 -24
- package/src/components/DeleteEntityDialog.tsx +4 -4
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +273 -277
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +1 -1
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +13 -13
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +10 -17
- package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +3 -3
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/default_entity_actions.tsx +9 -5
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +28 -49
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +5 -6
- package/src/components/EntityCollectionView/useSelectionController.tsx +30 -0
- package/src/components/EntityPreview.tsx +207 -70
- package/src/components/EntityView.tsx +84 -0
- package/src/components/FieldCaption.tsx +14 -0
- package/src/components/FireCMSAppBar.tsx +8 -0
- package/src/components/HomePage/DefaultHomePage.tsx +14 -10
- package/src/components/HomePage/NavigationCard.tsx +69 -0
- package/src/components/HomePage/NavigationCardBinding.tsx +116 -0
- package/src/components/HomePage/SmallNavigationCard.tsx +45 -0
- package/src/components/HomePage/index.tsx +3 -1
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +3 -4
- package/src/components/ReferenceWidget.tsx +8 -8
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +11 -19
- package/src/components/VirtualTable/VirtualTableProps.tsx +1 -1
- package/src/components/common/useDataSourceEntityCollectionTableController.tsx +1 -1
- package/src/components/index.tsx +4 -3
- package/src/contexts/AuthControllerContext.tsx +1 -1
- package/src/core/Drawer.tsx +66 -39
- package/src/{internal/EntityView.tsx → core/EntityEditView.tsx} +22 -39
- package/src/core/EntitySidePanel.tsx +2 -2
- package/src/core/FireCMS.tsx +18 -2
- package/src/core/NavigationRoutes.tsx +8 -0
- package/src/core/SideEntityView.tsx +38 -0
- package/src/core/index.tsx +0 -2
- package/src/form/EntityForm.tsx +20 -12
- package/src/form/components/StorageItemPreview.tsx +5 -3
- package/src/form/components/StorageUploadProgress.tsx +6 -5
- package/src/form/components/index.tsx +1 -0
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +2 -3
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +12 -15
- package/src/form/field_bindings/BlockFieldBinding.tsx +2 -3
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +3 -3
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +18 -18
- package/src/form/field_bindings/MapFieldBinding.tsx +17 -17
- package/src/form/field_bindings/MarkdownFieldBinding.tsx +1 -2
- package/src/form/field_bindings/MultiSelectBinding.tsx +2 -3
- package/src/form/field_bindings/ReadOnlyFieldBinding.tsx +3 -3
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +6 -4
- package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -3
- package/src/form/field_bindings/SelectFieldBinding.tsx +2 -3
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +15 -6
- package/src/form/field_bindings/SwitchFieldBinding.tsx +2 -3
- package/src/form/field_bindings/TextFieldBinding.tsx +10 -9
- package/src/form/index.tsx +1 -0
- package/src/form/validation.ts +3 -4
- package/src/hooks/data/delete.ts +3 -3
- package/src/hooks/data/save.ts +1 -1
- package/src/hooks/data/useCollectionFetch.tsx +1 -1
- package/src/hooks/data/useDataSource.tsx +8 -3
- package/src/hooks/data/useEntityFetch.tsx +4 -4
- package/src/hooks/index.tsx +5 -1
- package/src/{core → hooks}/useBuildModeController.tsx +1 -1
- package/src/hooks/useBuildNavigationController.tsx +135 -79
- package/src/hooks/useProjectLog.tsx +16 -6
- package/src/hooks/useReferenceDialog.tsx +2 -2
- package/src/hooks/useStorageSource.tsx +7 -2
- package/src/hooks/useValidateAuthenticator.tsx +135 -0
- package/src/internal/useBuildDataSource.ts +7 -2
- package/src/internal/useBuildSideEntityController.tsx +1 -0
- package/src/preview/PropertyPreview.tsx +2 -2
- package/src/preview/PropertyPreviewProps.tsx +1 -11
- package/src/preview/components/BooleanPreview.tsx +19 -4
- package/src/preview/components/EnumValuesChip.tsx +1 -1
- package/src/preview/components/ReferencePreview.tsx +56 -148
- package/src/preview/property_previews/StringPropertyPreview.tsx +8 -7
- package/src/types/analytics.ts +1 -0
- package/src/types/auth.tsx +50 -1
- package/src/types/collections.ts +19 -4
- package/src/types/datasource.ts +1 -1
- package/src/types/entities.ts +4 -0
- package/src/types/entity_actions.tsx +4 -0
- package/src/types/entity_callbacks.ts +2 -2
- package/src/types/entity_overrides.tsx +7 -0
- package/src/types/firecms.tsx +0 -1
- package/src/types/index.ts +2 -0
- package/src/types/navigation.ts +17 -16
- package/src/types/permissions.ts +6 -1
- package/src/types/plugins.tsx +24 -27
- package/src/types/properties.ts +3 -2
- package/src/types/property_config.tsx +2 -2
- package/src/types/roles.ts +41 -0
- package/src/types/side_entity_controller.tsx +1 -0
- package/src/types/user.ts +7 -0
- package/src/util/collections.ts +22 -0
- package/src/util/icons.tsx +11 -3
- package/src/util/permissions.ts +11 -8
- package/src/util/references.ts +36 -5
- package/src/util/useTraceUpdate.tsx +2 -1
- package/src/components/HomePage/NavigationCollectionCard.tsx +0 -146
- /package/dist/{components → form/components}/LabelWithIcon.d.ts +0 -0
- /package/dist/{core → hooks}/useBuildLocalConfigurationPersistence.d.ts +0 -0
- /package/src/{components → form/components}/LabelWithIcon.tsx +0 -0
- /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,
|
|
@@ -33,7 +34,10 @@ type BuildNavigationContextProps<EC extends EntityCollection, UserType extends U
|
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
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
|
-
|
|
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
|
|
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:
|
|
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,26 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
125
164
|
return;
|
|
126
165
|
|
|
127
166
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
167
|
+
console.debug("Refreshing navigation", {
|
|
168
|
+
authController
|
|
169
|
+
})
|
|
170
|
+
const [resolvedCollections = [], resolvedViews, resolvedAdminViews = []] = await Promise.all([
|
|
171
|
+
resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate, injectCollections),
|
|
172
|
+
resolveCMSViews(viewsProp, authController, dataSourceDelegate),
|
|
173
|
+
resolveCMSViews(adminViewsProp, authController, dataSourceDelegate)
|
|
131
174
|
]
|
|
132
175
|
);
|
|
133
|
-
|
|
176
|
+
|
|
177
|
+
if (
|
|
178
|
+
!equal(collectionsRef.current, resolvedCollections) ||
|
|
179
|
+
!equal(viewsRef.current, resolvedViews) ||
|
|
180
|
+
!equal(adminViewsRef.current, resolvedAdminViews) ||
|
|
181
|
+
!equal(topLevelNavigation, computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder))
|
|
182
|
+
) {
|
|
134
183
|
collectionsRef.current = resolvedCollections;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
setTopLevelNavigation(computeTopNavigation(resolvedCollections ?? [], resolvedViews));
|
|
184
|
+
viewsRef.current = resolvedViews;
|
|
185
|
+
adminViewsRef.current = resolvedAdminViews;
|
|
186
|
+
setTopLevelNavigation(computeTopNavigation(resolvedCollections ?? [], resolvedViews, resolvedAdminViews, viewsOrder));
|
|
138
187
|
}
|
|
139
188
|
} catch (e) {
|
|
140
189
|
console.error(e);
|
|
@@ -143,18 +192,28 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
143
192
|
|
|
144
193
|
setNavigationLoading(false);
|
|
145
194
|
setInitialised(true);
|
|
146
|
-
|
|
195
|
+
|
|
196
|
+
}, [
|
|
197
|
+
collectionsProp,
|
|
198
|
+
collectionPermissions,
|
|
199
|
+
authController.user,
|
|
200
|
+
authController.initialLoading,
|
|
201
|
+
viewsProp,
|
|
202
|
+
adminViewsProp,
|
|
203
|
+
computeTopNavigation,
|
|
204
|
+
injectCollections
|
|
205
|
+
]);
|
|
147
206
|
|
|
148
207
|
useEffect(() => {
|
|
149
208
|
refreshNavigation();
|
|
150
209
|
}, [refreshNavigation]);
|
|
151
210
|
|
|
152
|
-
const getCollection = useCallback(
|
|
211
|
+
const getCollection = useCallback((
|
|
153
212
|
idOrPath: string,
|
|
154
213
|
entityId?: string,
|
|
155
214
|
includeUserOverride = false
|
|
156
215
|
): EC | undefined => {
|
|
157
|
-
|
|
216
|
+
const collections = collectionsRef.current;
|
|
158
217
|
if (!collections)
|
|
159
218
|
return undefined;
|
|
160
219
|
|
|
@@ -182,14 +241,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
182
241
|
|
|
183
242
|
return { ...overriddenCollection, ...result } as EC;
|
|
184
243
|
|
|
185
|
-
}, [
|
|
186
|
-
basePath,
|
|
187
|
-
baseCollectionPath,
|
|
188
|
-
collections,
|
|
189
|
-
]);
|
|
244
|
+
}, [userConfigPersistence]);
|
|
190
245
|
|
|
191
246
|
const getCollectionFromPaths = useCallback(<EC extends EntityCollection>(pathSegments: string[]): EC | undefined => {
|
|
192
|
-
|
|
247
|
+
|
|
248
|
+
const collections = collectionsRef.current;
|
|
249
|
+
let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
|
|
193
250
|
if (!currentCollections)
|
|
194
251
|
throw Error("Collections have not been initialised yet");
|
|
195
252
|
|
|
@@ -205,10 +262,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
205
262
|
|
|
206
263
|
return undefined;
|
|
207
264
|
|
|
208
|
-
}, [
|
|
265
|
+
}, []);
|
|
209
266
|
|
|
210
267
|
const getCollectionFromIds = useCallback(<EC extends EntityCollection>(ids: string[]): EC | undefined => {
|
|
211
|
-
|
|
268
|
+
|
|
269
|
+
const collections = collectionsRef.current;
|
|
270
|
+
let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
|
|
212
271
|
if (!currentCollections)
|
|
213
272
|
throw Error("Collections have not been initialised yet");
|
|
214
273
|
|
|
@@ -224,7 +283,7 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
224
283
|
|
|
225
284
|
return undefined;
|
|
226
285
|
|
|
227
|
-
}, [
|
|
286
|
+
}, []);
|
|
228
287
|
|
|
229
288
|
const isUrlCollectionPath = useCallback(
|
|
230
289
|
(path: string): boolean => removeInitialAndTrailingSlashes(path + "/").startsWith(removeInitialAndTrailingSlashes(fullCollectionPath) + "/"),
|
|
@@ -246,25 +305,19 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
246
305
|
[]);
|
|
247
306
|
|
|
248
307
|
const resolveAliasesFrom = useCallback((path: string): string => {
|
|
308
|
+
const collections = collectionsRef.current;
|
|
249
309
|
if (!collections)
|
|
250
310
|
throw Error("Collections have not been initialised yet");
|
|
251
311
|
return resolveCollectionPathIds(path, collections);
|
|
252
|
-
}, [
|
|
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;
|
|
312
|
+
}, []);
|
|
261
313
|
|
|
262
314
|
const getAllParentReferencesForPath = useCallback((path: string): EntityReference[] => {
|
|
315
|
+
const collections = collectionsRef.current ?? [];
|
|
263
316
|
return getParentReferencesFromPath({
|
|
264
317
|
path,
|
|
265
318
|
collections
|
|
266
319
|
});
|
|
267
|
-
}, [
|
|
320
|
+
}, []);
|
|
268
321
|
|
|
269
322
|
const getParentCollectionIds = useCallback((path: string): string[] => {
|
|
270
323
|
|
|
@@ -279,35 +332,28 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
279
332
|
}
|
|
280
333
|
|
|
281
334
|
// for each odd path segment, get the collection
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
getCollectionFromPaths(oddPathSegments);
|
|
285
|
-
|
|
286
|
-
// const allParentCollectionsForPath = getAllParentReferencesForPath(path);
|
|
287
|
-
// console.log("allParentCollectionsForPath", allParentCollectionsForPath);
|
|
288
|
-
// const parentCollectionIds = allParentCollectionsForPath.map(r => r.id);
|
|
289
|
-
console.log("getParentCollectionIds", path, parentCollectionIds);
|
|
290
|
-
return parentCollectionIds;
|
|
335
|
+
return result.map(r => getCollectionFromPaths(r)?.id).filter(Boolean) as string[];
|
|
291
336
|
}, [getAllParentReferencesForPath])
|
|
292
337
|
|
|
293
338
|
const convertIdsToPaths = useCallback((ids: string[]): string[] => {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
return paths;
|
|
339
|
+
const collections = collectionsRef.current;
|
|
340
|
+
let currentCollections = collections;
|
|
341
|
+
const paths: string[] = [];
|
|
342
|
+
for (let i = 0; i < ids.length; i++) {
|
|
343
|
+
const id = ids[i];
|
|
344
|
+
const collection: EntityCollection | undefined = currentCollections!.find(c => c.id === id);
|
|
345
|
+
if (!collection)
|
|
346
|
+
throw Error(`Collection with id ${id} not found`);
|
|
347
|
+
paths.push(collection.path);
|
|
348
|
+
currentCollections = collection.subcollections;
|
|
305
349
|
}
|
|
306
|
-
|
|
350
|
+
return paths;
|
|
351
|
+
}, [getCollectionFromIds]);
|
|
307
352
|
|
|
308
353
|
return {
|
|
309
|
-
collections,
|
|
310
|
-
views,
|
|
354
|
+
collections: collectionsRef.current,
|
|
355
|
+
views: viewsRef.current,
|
|
356
|
+
adminViews: adminViewsRef.current,
|
|
311
357
|
loading: !initialised || navigationLoading,
|
|
312
358
|
navigationLoadingError,
|
|
313
359
|
homeUrl,
|
|
@@ -324,7 +370,6 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
324
370
|
buildCMSUrlPath,
|
|
325
371
|
resolveAliasesFrom,
|
|
326
372
|
topLevelNavigation,
|
|
327
|
-
baseLocation,
|
|
328
373
|
refreshNavigation,
|
|
329
374
|
getParentReferencesFromPath: getAllParentReferencesForPath,
|
|
330
375
|
getParentCollectionIds,
|
|
@@ -349,8 +394,8 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
|
|
|
349
394
|
return resolvedCollections
|
|
350
395
|
.filter((c) => {
|
|
351
396
|
if (!c.permissions) return true;
|
|
352
|
-
const resolvedPermissions = resolvePermissions(c, authController,
|
|
353
|
-
return resolvedPermissions
|
|
397
|
+
const resolvedPermissions = resolvePermissions(c, authController, c.path, null)
|
|
398
|
+
return resolvedPermissions?.read !== false;
|
|
354
399
|
})
|
|
355
400
|
.map((c) => {
|
|
356
401
|
if (!c.subcollections) return c;
|
|
@@ -362,6 +407,7 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
|
|
|
362
407
|
}
|
|
363
408
|
|
|
364
409
|
async function resolveCollections(collections: undefined | EntityCollection[] | EntityCollectionsBuilder<any>,
|
|
410
|
+
collectionPermissions: PermissionsBuilder | undefined,
|
|
365
411
|
authController: AuthController,
|
|
366
412
|
dataSource: DataSourceDelegate,
|
|
367
413
|
injectCollections?: (collections: EntityCollection[]) => EntityCollection[]) {
|
|
@@ -376,6 +422,8 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
|
|
|
376
422
|
resolvedCollections = collections;
|
|
377
423
|
}
|
|
378
424
|
|
|
425
|
+
resolvedCollections = applyPermissionsFunctionIfEmpty(resolvedCollections, collectionPermissions);
|
|
426
|
+
|
|
379
427
|
resolvedCollections = filterOutNotAllowedCollections(resolvedCollections, authController);
|
|
380
428
|
|
|
381
429
|
if (injectCollections) {
|
|
@@ -398,3 +446,11 @@ async function resolveCMSViews(baseViews: CMSView[] | CMSViewsBuilder | undefine
|
|
|
398
446
|
}
|
|
399
447
|
return resolvedViews;
|
|
400
448
|
}
|
|
449
|
+
|
|
450
|
+
function getGroup(collectionOrView: EntityCollection<any, any> | CMSView) {
|
|
451
|
+
const trimmed = collectionOrView.group?.trim();
|
|
452
|
+
if (!trimmed || trimmed === "") {
|
|
453
|
+
return "Views";
|
|
454
|
+
}
|
|
455
|
+
return trimmed ?? "Views";
|
|
456
|
+
}
|
|
@@ -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
|
-
|
|
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 {
|
|
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 =>
|
|
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
|
|
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);
|
|
@@ -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
|
|
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
|
}
|