@firecms/core 3.0.0-canary.4 → 3.0.0-canary.40
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 +2 -2
- package/dist/components/ClearFilterSortButton.d.ts +5 -0
- 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/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +1 -4
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +1 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +11 -0
- 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/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 +6 -4
- 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 +8343 -7846
- 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 +22 -5
- package/dist/types/datasource.d.ts +1 -1
- package/dist/types/entities.d.ts +1 -1
- 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 +20 -20
- 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/entities.d.ts +1 -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/ClearFilterSortButton.tsx +41 -0
- package/src/components/DeleteEntityDialog.tsx +4 -4
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +268 -277
- package/src/components/EntityCollectionTable/EntityCollectionTableProps.tsx +1 -1
- package/src/components/EntityCollectionTable/PropertyTableCell.tsx +13 -13
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +9 -16
- package/src/components/EntityCollectionTable/fields/TableStorageUpload.tsx +3 -3
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +27 -32
- package/src/components/EntityCollectionTable/internal/default_entity_actions.tsx +9 -5
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +39 -49
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +5 -6
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +68 -0
- 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 +4 -4
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +23 -8
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +35 -24
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +35 -15
- package/src/components/VirtualTable/VirtualTableProps.tsx +1 -1
- package/src/components/VirtualTable/fields/VirtualTableDateField.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/field_configs.tsx +1 -2
- 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 +4 -4
- 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 +5 -3
- 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}/useBuildLocalConfigurationPersistence.tsx +9 -10
- package/src/{core → hooks}/useBuildModeController.tsx +12 -6
- package/src/hooks/useBuildNavigationController.tsx +190 -72
- 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 +6 -1
- package/src/internal/useBuildSideEntityController.tsx +18 -12
- package/src/preview/PropertyPreview.tsx +1 -1
- 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 +55 -147
- 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 +24 -5
- package/src/types/datasource.ts +1 -1
- package/src/types/entities.ts +1 -1
- 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 +26 -28
- 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/entities.ts +1 -1
- package/src/util/icons.tsx +11 -3
- package/src/util/permissions.ts +11 -8
- package/src/util/references.ts +36 -5
- package/src/util/strings.ts +2 -2
- 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
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { useLocation } from "react-router-dom";
|
|
3
2
|
import equal from "react-fast-compare"
|
|
4
3
|
|
|
5
4
|
import {
|
|
@@ -11,12 +10,14 @@ import {
|
|
|
11
10
|
EntityCollectionsBuilder,
|
|
12
11
|
EntityReference,
|
|
13
12
|
NavigationController,
|
|
13
|
+
PermissionsBuilder,
|
|
14
14
|
TopNavigationEntry,
|
|
15
15
|
TopNavigationResult,
|
|
16
16
|
User,
|
|
17
17
|
UserConfigurationPersistence
|
|
18
18
|
} from "../types";
|
|
19
19
|
import {
|
|
20
|
+
applyPermissionsFunctionIfEmpty,
|
|
20
21
|
getCollectionByPathOrId,
|
|
21
22
|
mergeDeep,
|
|
22
23
|
removeInitialAndTrailingSlashes,
|
|
@@ -28,12 +29,15 @@ import { getParentReferencesFromPath } from "../util/parent_references_from_path
|
|
|
28
29
|
const DEFAULT_BASE_PATH = "/";
|
|
29
30
|
const DEFAULT_COLLECTION_PATH = "/c";
|
|
30
31
|
|
|
31
|
-
type BuildNavigationContextProps<EC extends EntityCollection, UserType extends User> = {
|
|
32
|
+
export type BuildNavigationContextProps<EC extends EntityCollection, UserType extends User> = {
|
|
32
33
|
basePath?: string,
|
|
33
34
|
baseCollectionPath?: string,
|
|
34
35
|
authController: AuthController<UserType>;
|
|
35
36
|
collections?: EC[] | EntityCollectionsBuilder<EC>;
|
|
37
|
+
collectionPermissions?: PermissionsBuilder;
|
|
36
38
|
views?: CMSView[] | CMSViewsBuilder;
|
|
39
|
+
adminViews?: CMSView[] | CMSViewsBuilder;
|
|
40
|
+
viewsOrder?: string[];
|
|
37
41
|
userConfigPersistence?: UserConfigurationPersistence;
|
|
38
42
|
dataSourceDelegate: DataSourceDelegate;
|
|
39
43
|
/**
|
|
@@ -46,22 +50,25 @@ type BuildNavigationContextProps<EC extends EntityCollection, UserType extends U
|
|
|
46
50
|
injectCollections?: (collections: EntityCollection[]) => EntityCollection[];
|
|
47
51
|
};
|
|
48
52
|
|
|
49
|
-
export function useBuildNavigationController<EC extends EntityCollection, UserType extends User>({
|
|
50
|
-
|
|
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,74 @@ 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
|
+
// Sort by group, entries with group "Admin" will go last, and second to last will be the group "Views"
|
|
134
|
+
navigationEntries = navigationEntries.sort((a, b) => {
|
|
135
|
+
if (a.group !== "Views" && a.group !== "Admin" && (b.group === "Views" || b.group === "Admin")) {
|
|
136
|
+
return -1;
|
|
137
|
+
}
|
|
138
|
+
if (b.group !== "Views" && b.group !== "Admin" && (a.group === "Views" || a.group === "Admin")) {
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
if (a.group === "Admin" && b.group !== "Admin") {
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
if (a.group !== "Admin" && b.group === "Admin") {
|
|
145
|
+
return -1;
|
|
146
|
+
}
|
|
147
|
+
if (a.group === "Views" && b.group !== "Views") {
|
|
148
|
+
return -1;
|
|
149
|
+
}
|
|
150
|
+
if (a.group !== "Views" && b.group === "Views") {
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
return 0;
|
|
154
|
+
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (viewsOrder) {
|
|
158
|
+
navigationEntries = navigationEntries.sort((a, b) => {
|
|
159
|
+
const aIndex = viewsOrder.indexOf(a.path);
|
|
160
|
+
const bIndex = viewsOrder.indexOf(b.path);
|
|
161
|
+
if (aIndex === -1 && bIndex === -1) {
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
if (aIndex === -1) {
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
if (bIndex === -1) {
|
|
168
|
+
return -1;
|
|
169
|
+
}
|
|
170
|
+
return aIndex - bIndex;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
112
174
|
const groups: string[] = Object.values(navigationEntries)
|
|
113
175
|
.map(e => e.group)
|
|
114
176
|
.filter(Boolean)
|
|
115
177
|
.filter((value, index, array) => array.indexOf(value) === index) as string[];
|
|
178
|
+
|
|
116
179
|
return {
|
|
117
180
|
navigationEntries,
|
|
118
181
|
groups
|
|
@@ -124,17 +187,34 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
124
187
|
if (authController.initialLoading)
|
|
125
188
|
return;
|
|
126
189
|
|
|
190
|
+
console.debug("Refreshing navigation");
|
|
191
|
+
|
|
127
192
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
193
|
+
|
|
194
|
+
const [resolvedCollections = [], resolvedViews, resolvedAdminViews = []] = await Promise.all([
|
|
195
|
+
resolveCollections(collectionsProp, collectionPermissions, authController, dataSourceDelegate, injectCollections),
|
|
196
|
+
resolveCMSViews(viewsProp, authController, dataSourceDelegate),
|
|
197
|
+
resolveCMSViews(adminViewsProp, authController, dataSourceDelegate)
|
|
131
198
|
]
|
|
132
199
|
);
|
|
133
|
-
|
|
200
|
+
|
|
201
|
+
let shouldUpdateTopLevelNav = false;
|
|
202
|
+
if (!areCollectionListsEqual(collectionsRef.current ?? [], resolvedCollections)) {
|
|
134
203
|
collectionsRef.current = resolvedCollections;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
204
|
+
shouldUpdateTopLevelNav = true;
|
|
205
|
+
}
|
|
206
|
+
if (!equal(viewsRef.current, resolvedViews)) {
|
|
207
|
+
viewsRef.current = resolvedViews;
|
|
208
|
+
shouldUpdateTopLevelNav = true;
|
|
209
|
+
}
|
|
210
|
+
if (!equal(adminViewsRef.current, resolvedAdminViews)) {
|
|
211
|
+
adminViewsRef.current = resolvedAdminViews;
|
|
212
|
+
shouldUpdateTopLevelNav = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const computedTopLevelNav = computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder);
|
|
216
|
+
if (shouldUpdateTopLevelNav && !equal(topLevelNavigation, computedTopLevelNav)) {
|
|
217
|
+
setTopLevelNavigation(computedTopLevelNav);
|
|
138
218
|
}
|
|
139
219
|
} catch (e) {
|
|
140
220
|
console.error(e);
|
|
@@ -143,25 +223,34 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
143
223
|
|
|
144
224
|
setNavigationLoading(false);
|
|
145
225
|
setInitialised(true);
|
|
146
|
-
|
|
226
|
+
|
|
227
|
+
}, [
|
|
228
|
+
collectionsProp,
|
|
229
|
+
collectionPermissions,
|
|
230
|
+
authController.user,
|
|
231
|
+
authController.initialLoading,
|
|
232
|
+
viewsProp,
|
|
233
|
+
adminViewsProp,
|
|
234
|
+
computeTopNavigation,
|
|
235
|
+
injectCollections
|
|
236
|
+
]);
|
|
147
237
|
|
|
148
238
|
useEffect(() => {
|
|
149
239
|
refreshNavigation();
|
|
150
240
|
}, [refreshNavigation]);
|
|
151
241
|
|
|
152
|
-
const getCollection = useCallback(
|
|
242
|
+
const getCollection = useCallback((
|
|
153
243
|
idOrPath: string,
|
|
154
244
|
entityId?: string,
|
|
155
245
|
includeUserOverride = false
|
|
156
246
|
): EC | undefined => {
|
|
157
|
-
|
|
247
|
+
const collections = collectionsRef.current;
|
|
158
248
|
if (!collections)
|
|
159
249
|
return undefined;
|
|
160
250
|
|
|
161
251
|
const baseCollection = getCollectionByPathOrId(removeInitialAndTrailingSlashes(idOrPath), collections);
|
|
162
252
|
|
|
163
253
|
const userOverride = includeUserOverride ? userConfigPersistence?.getCollectionConfig(idOrPath) : undefined;
|
|
164
|
-
|
|
165
254
|
const overriddenCollection = baseCollection ? mergeDeep(baseCollection, userOverride) : undefined;
|
|
166
255
|
|
|
167
256
|
let result: Partial<EntityCollection> | undefined = overriddenCollection;
|
|
@@ -182,14 +271,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
182
271
|
|
|
183
272
|
return { ...overriddenCollection, ...result } as EC;
|
|
184
273
|
|
|
185
|
-
}, [
|
|
186
|
-
basePath,
|
|
187
|
-
baseCollectionPath,
|
|
188
|
-
collections,
|
|
189
|
-
]);
|
|
274
|
+
}, [userConfigPersistence]);
|
|
190
275
|
|
|
191
276
|
const getCollectionFromPaths = useCallback(<EC extends EntityCollection>(pathSegments: string[]): EC | undefined => {
|
|
192
|
-
|
|
277
|
+
|
|
278
|
+
const collections = collectionsRef.current;
|
|
279
|
+
let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
|
|
193
280
|
if (!currentCollections)
|
|
194
281
|
throw Error("Collections have not been initialised yet");
|
|
195
282
|
|
|
@@ -205,10 +292,12 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
205
292
|
|
|
206
293
|
return undefined;
|
|
207
294
|
|
|
208
|
-
}, [
|
|
295
|
+
}, []);
|
|
209
296
|
|
|
210
297
|
const getCollectionFromIds = useCallback(<EC extends EntityCollection>(ids: string[]): EC | undefined => {
|
|
211
|
-
|
|
298
|
+
|
|
299
|
+
const collections = collectionsRef.current;
|
|
300
|
+
let currentCollections: EntityCollection[] | undefined = [...(collections ?? [])];
|
|
212
301
|
if (!currentCollections)
|
|
213
302
|
throw Error("Collections have not been initialised yet");
|
|
214
303
|
|
|
@@ -224,7 +313,7 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
224
313
|
|
|
225
314
|
return undefined;
|
|
226
315
|
|
|
227
|
-
}, [
|
|
316
|
+
}, []);
|
|
228
317
|
|
|
229
318
|
const isUrlCollectionPath = useCallback(
|
|
230
319
|
(path: string): boolean => removeInitialAndTrailingSlashes(path + "/").startsWith(removeInitialAndTrailingSlashes(fullCollectionPath) + "/"),
|
|
@@ -246,25 +335,19 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
246
335
|
[]);
|
|
247
336
|
|
|
248
337
|
const resolveAliasesFrom = useCallback((path: string): string => {
|
|
338
|
+
const collections = collectionsRef.current;
|
|
249
339
|
if (!collections)
|
|
250
340
|
throw Error("Collections have not been initialised yet");
|
|
251
341
|
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;
|
|
342
|
+
}, []);
|
|
261
343
|
|
|
262
344
|
const getAllParentReferencesForPath = useCallback((path: string): EntityReference[] => {
|
|
345
|
+
const collections = collectionsRef.current ?? [];
|
|
263
346
|
return getParentReferencesFromPath({
|
|
264
347
|
path,
|
|
265
348
|
collections
|
|
266
349
|
});
|
|
267
|
-
}, [
|
|
350
|
+
}, []);
|
|
268
351
|
|
|
269
352
|
const getParentCollectionIds = useCallback((path: string): string[] => {
|
|
270
353
|
|
|
@@ -283,23 +366,24 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
283
366
|
}, [getAllParentReferencesForPath])
|
|
284
367
|
|
|
285
368
|
const convertIdsToPaths = useCallback((ids: string[]): string[] => {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return paths;
|
|
369
|
+
const collections = collectionsRef.current;
|
|
370
|
+
let currentCollections = collections;
|
|
371
|
+
const paths: string[] = [];
|
|
372
|
+
for (let i = 0; i < ids.length; i++) {
|
|
373
|
+
const id = ids[i];
|
|
374
|
+
const collection: EntityCollection | undefined = currentCollections!.find(c => c.id === id);
|
|
375
|
+
if (!collection)
|
|
376
|
+
throw Error(`Collection with id ${id} not found`);
|
|
377
|
+
paths.push(collection.path);
|
|
378
|
+
currentCollections = collection.subcollections;
|
|
297
379
|
}
|
|
298
|
-
|
|
380
|
+
return paths;
|
|
381
|
+
}, [getCollectionFromIds]);
|
|
299
382
|
|
|
300
383
|
return {
|
|
301
|
-
collections,
|
|
302
|
-
views,
|
|
384
|
+
collections: collectionsRef.current,
|
|
385
|
+
views: viewsRef.current,
|
|
386
|
+
adminViews: adminViewsRef.current,
|
|
303
387
|
loading: !initialised || navigationLoading,
|
|
304
388
|
navigationLoadingError,
|
|
305
389
|
homeUrl,
|
|
@@ -316,7 +400,6 @@ export function useBuildNavigationController<EC extends EntityCollection, UserTy
|
|
|
316
400
|
buildCMSUrlPath,
|
|
317
401
|
resolveAliasesFrom,
|
|
318
402
|
topLevelNavigation,
|
|
319
|
-
baseLocation,
|
|
320
403
|
refreshNavigation,
|
|
321
404
|
getParentReferencesFromPath: getAllParentReferencesForPath,
|
|
322
405
|
getParentCollectionIds,
|
|
@@ -341,8 +424,8 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
|
|
|
341
424
|
return resolvedCollections
|
|
342
425
|
.filter((c) => {
|
|
343
426
|
if (!c.permissions) return true;
|
|
344
|
-
const resolvedPermissions = resolvePermissions(c, authController,
|
|
345
|
-
return resolvedPermissions
|
|
427
|
+
const resolvedPermissions = resolvePermissions(c, authController, c.path, null)
|
|
428
|
+
return resolvedPermissions?.read !== false;
|
|
346
429
|
})
|
|
347
430
|
.map((c) => {
|
|
348
431
|
if (!c.subcollections) return c;
|
|
@@ -354,6 +437,7 @@ function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[],
|
|
|
354
437
|
}
|
|
355
438
|
|
|
356
439
|
async function resolveCollections(collections: undefined | EntityCollection[] | EntityCollectionsBuilder<any>,
|
|
440
|
+
collectionPermissions: PermissionsBuilder | undefined,
|
|
357
441
|
authController: AuthController,
|
|
358
442
|
dataSource: DataSourceDelegate,
|
|
359
443
|
injectCollections?: (collections: EntityCollection[]) => EntityCollection[]) {
|
|
@@ -368,6 +452,8 @@ async function resolveCollections(collections: undefined | EntityCollection[] |
|
|
|
368
452
|
resolvedCollections = collections;
|
|
369
453
|
}
|
|
370
454
|
|
|
455
|
+
resolvedCollections = applyPermissionsFunctionIfEmpty(resolvedCollections, collectionPermissions);
|
|
456
|
+
|
|
371
457
|
resolvedCollections = filterOutNotAllowedCollections(resolvedCollections, authController);
|
|
372
458
|
|
|
373
459
|
if (injectCollections) {
|
|
@@ -390,3 +476,35 @@ async function resolveCMSViews(baseViews: CMSView[] | CMSViewsBuilder | undefine
|
|
|
390
476
|
}
|
|
391
477
|
return resolvedViews;
|
|
392
478
|
}
|
|
479
|
+
|
|
480
|
+
function getGroup(collectionOrView: EntityCollection<any, any> | CMSView) {
|
|
481
|
+
const trimmed = collectionOrView.group?.trim();
|
|
482
|
+
if (!trimmed || trimmed === "") {
|
|
483
|
+
return "Views";
|
|
484
|
+
}
|
|
485
|
+
return trimmed ?? "Views";
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function areCollectionListsEqual(a: EntityCollection[], b: EntityCollection[]) {
|
|
489
|
+
if (a.length !== b.length) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
const aSorted = a.sort((a, b) => a.id.localeCompare(b.id));
|
|
493
|
+
const bSorted = b.sort((a, b) => a.id.localeCompare(b.id));
|
|
494
|
+
return aSorted.every((value, index) => areCollectionsEqual(value, bSorted[index]));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function areCollectionsEqual(a: EntityCollection, b: EntityCollection) {
|
|
498
|
+
const {
|
|
499
|
+
subcollections: subcollectionsA,
|
|
500
|
+
...restA
|
|
501
|
+
} = a;
|
|
502
|
+
const {
|
|
503
|
+
subcollections: subcollectionsB,
|
|
504
|
+
...restB
|
|
505
|
+
} = b;
|
|
506
|
+
if (!areCollectionListsEqual(subcollectionsA ?? [], subcollectionsB ?? [])) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
return equal(restA, restB);
|
|
510
|
+
}
|
|
@@ -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
|
+
}
|