@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.
- package/README.md +1 -1
- package/dist/components/EntityCollectionTable/EntityCollectionRowActions.d.ts +3 -7
- 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 +8575 -8207
- 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/storage.d.ts +11 -3
- 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/index.d.ts +1 -0
- 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 +31 -23
- package/src/components/DeleteEntityDialog.tsx +4 -4
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +35 -39
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +276 -282
- 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 +37 -58
- 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 +153 -76
- 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 +3 -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/storage.ts +12 -3
- package/src/types/user.ts +7 -0
- package/src/util/collections.ts +22 -0
- package/src/util/icons.tsx +11 -3
- package/src/util/index.ts +1 -0
- 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,5 @@
|
|
|
1
|
-
import { useCallback, useEffect,
|
|
2
|
-
import
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
}, [
|
|
263
|
+
}, []);
|
|
206
264
|
|
|
207
265
|
const getCollectionFromIds = useCallback(<EC extends EntityCollection>(ids: string[]): EC | undefined => {
|
|
208
|
-
|
|
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
|
-
}, [
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
}, [
|
|
318
|
+
}, []);
|
|
265
319
|
|
|
266
320
|
const getParentCollectionIds = useCallback((path: string): string[] => {
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
348
|
+
return paths;
|
|
349
|
+
}, [getCollectionFromIds]);
|
|
284
350
|
|
|
285
|
-
return
|
|
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:
|
|
372
|
+
getParentReferencesFromPath: getAllParentReferencesForPath,
|
|
307
373
|
getParentCollectionIds,
|
|
308
374
|
convertIdsToPaths
|
|
309
|
-
}
|
|
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,
|
|
330
|
-
return resolvedPermissions
|
|
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
|
-
|
|
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);
|
|
@@ -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
|
}
|