@firecms/core 3.0.0-canary.3 → 3.0.0-canary.30
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 +8055 -7703
- 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/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 +132 -70
- 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/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,
|
|
104
112
|
view,
|
|
105
113
|
description: view.description?.trim(),
|
|
106
|
-
group: view
|
|
107
|
-
})
|
|
114
|
+
group: getGroup(view)
|
|
115
|
+
} satisfies TopNavigationEntry)
|
|
116
|
+
: undefined)
|
|
117
|
+
.filter(Boolean) as TopNavigationEntry[],
|
|
118
|
+
...(adminViews ?? []).map(view =>
|
|
119
|
+
!view.hideFromNavigation
|
|
120
|
+
? ({
|
|
121
|
+
url: buildCMSUrlPath(Array.isArray(view.path) ? view.path[0] : view.path),
|
|
122
|
+
name: view.name.trim(),
|
|
123
|
+
type: "admin",
|
|
124
|
+
path: view.path,
|
|
125
|
+
view,
|
|
126
|
+
description: view.description?.trim(),
|
|
127
|
+
group: "Admin"
|
|
128
|
+
} satisfies TopNavigationEntry)
|
|
108
129
|
: undefined)
|
|
109
130
|
.filter(Boolean) as TopNavigationEntry[]
|
|
110
131
|
];
|
|
111
132
|
|
|
133
|
+
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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,18 +190,28 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
143
190
|
|
|
144
191
|
setNavigationLoading(false);
|
|
145
192
|
setInitialised(true);
|
|
146
|
-
|
|
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(
|
|
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
|
|
|
@@ -182,14 +239,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
182
239
|
|
|
183
240
|
return { ...overriddenCollection, ...result } as EC;
|
|
184
241
|
|
|
185
|
-
}, [
|
|
186
|
-
basePath,
|
|
187
|
-
baseCollectionPath,
|
|
188
|
-
collections,
|
|
189
|
-
]);
|
|
242
|
+
}, [userConfigPersistence]);
|
|
190
243
|
|
|
191
244
|
const getCollectionFromPaths = useCallback(<EC extends EntityCollection>(pathSegments: string[]): EC | undefined => {
|
|
192
|
-
|
|
245
|
+
|
|
246
|
+
const collections = collectionsRef.current;
|
|
247
|
+
let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
|
|
193
248
|
if (!currentCollections)
|
|
194
249
|
throw Error("Collections have not been initialised yet");
|
|
195
250
|
|
|
@@ -205,10 +260,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
205
260
|
|
|
206
261
|
return undefined;
|
|
207
262
|
|
|
208
|
-
}, [
|
|
263
|
+
}, []);
|
|
209
264
|
|
|
210
265
|
const getCollectionFromIds = useCallback(<EC extends EntityCollection>(ids: string[]): EC | undefined => {
|
|
211
|
-
|
|
266
|
+
|
|
267
|
+
const collections = collectionsRef.current;
|
|
268
|
+
let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
|
|
212
269
|
if (!currentCollections)
|
|
213
270
|
throw Error("Collections have not been initialised yet");
|
|
214
271
|
|
|
@@ -224,7 +281,7 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
224
281
|
|
|
225
282
|
return undefined;
|
|
226
283
|
|
|
227
|
-
}, [
|
|
284
|
+
}, []);
|
|
228
285
|
|
|
229
286
|
const isUrlCollectionPath = useCallback(
|
|
230
287
|
(path: string): boolean => removeInitialAndTrailingSlashes(path + "/").startsWith(removeInitialAndTrailingSlashes(fullCollectionPath) + "/"),
|
|
@@ -246,25 +303,19 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
246
303
|
[]);
|
|
247
304
|
|
|
248
305
|
const resolveAliasesFrom = useCallback((path: string): string => {
|
|
306
|
+
const collections = collectionsRef.current;
|
|
249
307
|
if (!collections)
|
|
250
308
|
throw Error("Collections have not been initialised yet");
|
|
251
309
|
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;
|
|
310
|
+
}, []);
|
|
261
311
|
|
|
262
312
|
const getAllParentReferencesForPath = useCallback((path: string): EntityReference[] => {
|
|
313
|
+
const collections = collectionsRef.current ?? [];
|
|
263
314
|
return getParentReferencesFromPath({
|
|
264
315
|
path,
|
|
265
316
|
collections
|
|
266
317
|
});
|
|
267
|
-
}, [
|
|
318
|
+
}, []);
|
|
268
319
|
|
|
269
320
|
const getParentCollectionIds = useCallback((path: string): string[] => {
|
|
270
321
|
|
|
@@ -283,23 +334,24 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
283
334
|
}, [getAllParentReferencesForPath])
|
|
284
335
|
|
|
285
336
|
const convertIdsToPaths = useCallback((ids: string[]): string[] => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
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;
|
|
297
347
|
}
|
|
298
|
-
|
|
348
|
+
return paths;
|
|
349
|
+
}, [getCollectionFromIds]);
|
|
299
350
|
|
|
300
351
|
return {
|
|
301
|
-
collections,
|
|
302
|
-
views,
|
|
352
|
+
collections: collectionsRef.current,
|
|
353
|
+
views: viewsRef.current,
|
|
354
|
+
adminViews: adminViewsRef.current,
|
|
303
355
|
loading: !initialised || navigationLoading,
|
|
304
356
|
navigationLoadingError,
|
|
305
357
|
homeUrl,
|
|
@@ -316,7 +368,6 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
316
368
|
buildCMSUrlPath,
|
|
317
369
|
resolveAliasesFrom,
|
|
318
370
|
topLevelNavigation,
|
|
319
|
-
baseLocation,
|
|
320
371
|
refreshNavigation,
|
|
321
372
|
getParentReferencesFromPath: getAllParentReferencesForPath,
|
|
322
373
|
getParentCollectionIds,
|
|
@@ -341,8 +392,8 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
|
|
|
341
392
|
return resolvedCollections
|
|
342
393
|
.filter((c) => {
|
|
343
394
|
if (!c.permissions) return true;
|
|
344
|
-
const resolvedPermissions = resolvePermissions(c, authController,
|
|
345
|
-
return resolvedPermissions
|
|
395
|
+
const resolvedPermissions = resolvePermissions(c, authController, c.path, null)
|
|
396
|
+
return resolvedPermissions?.read !== false;
|
|
346
397
|
})
|
|
347
398
|
.map((c) => {
|
|
348
399
|
if (!c.subcollections) return c;
|
|
@@ -354,6 +405,7 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
|
|
|
354
405
|
}
|
|
355
406
|
|
|
356
407
|
async function resolveCollections(collections: undefined | EntityCollection[] | EntityCollectionsBuilder<any>,
|
|
408
|
+
collectionPermissions: PermissionsBuilder | undefined,
|
|
357
409
|
authController: AuthController,
|
|
358
410
|
dataSource: DataSourceDelegate,
|
|
359
411
|
injectCollections?: (collections: EntityCollection[]) => EntityCollection[]) {
|
|
@@ -368,6 +420,8 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
|
|
|
368
420
|
resolvedCollections = collections;
|
|
369
421
|
}
|
|
370
422
|
|
|
423
|
+
resolvedCollections = applyPermissionsFunctionIfEmpty(resolvedCollections, collectionPermissions);
|
|
424
|
+
|
|
371
425
|
resolvedCollections = filterOutNotAllowedCollections(resolvedCollections, authController);
|
|
372
426
|
|
|
373
427
|
if (injectCollections) {
|
|
@@ -390,3 +444,11 @@ async function resolveCMSViews(baseViews: CMSView[] | CMSViewsBuilder | undefine
|
|
|
390
444
|
}
|
|
391
445
|
return resolvedViews;
|
|
392
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
|
}
|
|
@@ -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
|
}
|