@firecms/core 3.4.0-canary.63041d0 → 3.4.0-canary.cfeed0b
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/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +3 -2
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
- package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
- package/dist/components/common/useDataSourceTableController.d.ts +4 -2
- package/dist/components/common/useTableSearchHelper.d.ts +3 -1
- package/dist/core/EntityEditView.d.ts +2 -0
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/form/EntityForm.d.ts +8 -1
- package/dist/form/EntityFormActions.d.ts +2 -0
- package/dist/hooks/data/delete.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
- package/dist/hooks/data/useEntityFetch.d.ts +4 -1
- package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
- package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
- package/dist/index.es.js +967 -649
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +966 -648
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collections.d.ts +7 -0
- package/dist/types/datasource.d.ts +25 -4
- package/dist/types/entities.d.ts +21 -1
- package/dist/types/entity_actions.d.ts +6 -0
- package/dist/types/entity_callbacks.d.ts +21 -0
- package/dist/types/navigation.d.ts +34 -5
- package/dist/types/plugins.d.ts +13 -0
- package/dist/types/side_entity_controller.d.ts +3 -2
- package/dist/util/entity_cache.d.ts +11 -0
- package/dist/util/navigation_utils.d.ts +84 -2
- package/dist/util/parent_references_from_path.d.ts +16 -0
- package/dist/util/paths.d.ts +28 -0
- package/dist/util/permissions.d.ts +10 -4
- package/package.json +4 -4
- package/src/components/DeleteEntityDialog.tsx +2 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +49 -16
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
- package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
- package/src/components/EntityPreview.tsx +4 -1
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
- package/src/components/common/default_entity_actions.tsx +22 -2
- package/src/components/common/useDataSourceTableController.tsx +17 -5
- package/src/components/common/useTableSearchHelper.ts +5 -0
- package/src/core/EntityEditView.tsx +11 -12
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +6 -4
- package/src/form/EntityForm.tsx +30 -9
- package/src/form/EntityFormActions.tsx +2 -0
- package/src/hooks/data/delete.ts +8 -0
- package/src/hooks/data/save.ts +5 -2
- package/src/hooks/data/useCollectionFetch.tsx +21 -1
- package/src/hooks/data/useEntityFetch.tsx +28 -6
- package/src/hooks/useBuildNavigationController.tsx +23 -8
- package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
- package/src/i18n/FireCMSi18nProvider.tsx +48 -4
- package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
- package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
- package/src/internal/useBuildDataSource.ts +26 -15
- package/src/internal/useBuildSideEntityController.tsx +13 -5
- package/src/preview/components/ReferencePreview.tsx +7 -3
- package/src/routes/FireCMSRoute.tsx +9 -4
- package/src/types/collections.ts +8 -0
- package/src/types/datasource.ts +25 -4
- package/src/types/entities.ts +24 -1
- package/src/types/entity_actions.tsx +6 -0
- package/src/types/entity_callbacks.ts +24 -0
- package/src/types/navigation.ts +35 -5
- package/src/types/plugins.tsx +13 -0
- package/src/types/side_entity_controller.tsx +3 -2
- package/src/util/entities.ts +3 -1
- package/src/util/entity_cache.ts +18 -0
- package/src/util/navigation_from_path.ts +6 -1
- package/src/util/navigation_utils.ts +204 -2
- package/src/util/parent_references_from_path.ts +42 -3
- package/src/util/paths.ts +40 -3
- package/src/util/permissions.ts +22 -10
package/src/types/datasource.ts
CHANGED
|
@@ -106,6 +106,11 @@ export interface SaveEntityProps<M extends Record<string, any> = any> {
|
|
|
106
106
|
*/
|
|
107
107
|
export interface DeleteEntityProps<M extends Record<string, any> = any> {
|
|
108
108
|
entity: Entity<M>;
|
|
109
|
+
/**
|
|
110
|
+
* `entity.path` split at its real segment boundaries, when the caller knows them.
|
|
111
|
+
* Never derived from `entity.path` — see `pathSegments` on FetchCollectionProps.
|
|
112
|
+
*/
|
|
113
|
+
pathSegments?: string[];
|
|
109
114
|
collection?: EntityCollection<M> | ResolvedEntityCollection<M>;
|
|
110
115
|
}
|
|
111
116
|
|
|
@@ -245,13 +250,19 @@ export interface DataSource {
|
|
|
245
250
|
name: string,
|
|
246
251
|
value: any,
|
|
247
252
|
entityId?: string,
|
|
248
|
-
collection?: EntityCollection
|
|
253
|
+
collection?: EntityCollection,
|
|
254
|
+
/**
|
|
255
|
+
* Appended last, and optional, so existing implementations and callers keep
|
|
256
|
+
* working unchanged — a function declaring fewer parameters stays assignable.
|
|
257
|
+
* See `pathSegments` on FetchCollectionProps.
|
|
258
|
+
*/
|
|
259
|
+
pathSegments?: string[]
|
|
249
260
|
): Promise<boolean>;
|
|
250
261
|
|
|
251
262
|
/**
|
|
252
263
|
* Generate an id for a new entity
|
|
253
264
|
*/
|
|
254
|
-
generateEntityId(path: string, collection: EntityCollection): string;
|
|
265
|
+
generateEntityId(path: string, collection: EntityCollection, pathSegments?: string[]): string;
|
|
255
266
|
|
|
256
267
|
/**
|
|
257
268
|
* Count the number of entities in a collection
|
|
@@ -272,6 +283,10 @@ export interface DataSource {
|
|
|
272
283
|
initTextSearch?: (props: {
|
|
273
284
|
context: FireCMSContext,
|
|
274
285
|
path: string,
|
|
286
|
+
/**
|
|
287
|
+
* `path` split at its real segment boundaries, when known. Never derived from `path`.
|
|
288
|
+
*/
|
|
289
|
+
pathSegments?: string[],
|
|
275
290
|
collection: EntityCollection,
|
|
276
291
|
parentCollectionIds?: string[]
|
|
277
292
|
}) => Promise<boolean>;
|
|
@@ -279,6 +294,8 @@ export interface DataSource {
|
|
|
279
294
|
}
|
|
280
295
|
|
|
281
296
|
export type FilterCombinationValidProps = {
|
|
297
|
+
/** `path` split at its real segment boundaries — see `pathSegments` on FetchCollectionProps. */
|
|
298
|
+
pathSegments?: string[];
|
|
282
299
|
path: string;
|
|
283
300
|
collection: EntityCollection<any>;
|
|
284
301
|
filterValues: FilterValues<any>;
|
|
@@ -408,12 +425,12 @@ export interface DataSourceDelegate {
|
|
|
408
425
|
* @param collection
|
|
409
426
|
* @return `true` if there are no other fields besides the given entity
|
|
410
427
|
*/
|
|
411
|
-
checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection): Promise<boolean>;
|
|
428
|
+
checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection, pathSegments?: string[]): Promise<boolean>;
|
|
412
429
|
|
|
413
430
|
/**
|
|
414
431
|
* Generate an id for a new entity
|
|
415
432
|
*/
|
|
416
|
-
generateEntityId(path: string, collection?: EntityCollection): string;
|
|
433
|
+
generateEntityId(path: string, collection?: EntityCollection, pathSegments?: string[]): string;
|
|
417
434
|
|
|
418
435
|
/**
|
|
419
436
|
* Count the number of entities in a collection
|
|
@@ -440,6 +457,10 @@ export interface DataSourceDelegate {
|
|
|
440
457
|
initTextSearch?: (props: {
|
|
441
458
|
context: FireCMSContext,
|
|
442
459
|
path: string,
|
|
460
|
+
/**
|
|
461
|
+
* `path` split at its real segment boundaries, when known. Never derived from `path`.
|
|
462
|
+
*/
|
|
463
|
+
pathSegments?: string[],
|
|
443
464
|
databaseId?: string,
|
|
444
465
|
collection: EntityCollection,
|
|
445
466
|
parentCollectionIds?: string[]
|
package/src/types/entities.ts
CHANGED
|
@@ -21,6 +21,17 @@ export interface Entity<M extends object = any> {
|
|
|
21
21
|
*/
|
|
22
22
|
path: string;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
26
|
+
*
|
|
27
|
+
* `path` is a single flattened string, so a parent entity id containing "/" cannot be
|
|
28
|
+
* recovered from it. This keeps each segment whole.
|
|
29
|
+
*
|
|
30
|
+
* Optional, and never derived by splitting `path` — a guess would be wrong in exactly
|
|
31
|
+
* the case the field exists for. Absent means "not known here", not "no slashes".
|
|
32
|
+
*/
|
|
33
|
+
pathSegments?: string[];
|
|
34
|
+
|
|
24
35
|
/**
|
|
25
36
|
* Current values
|
|
26
37
|
*/
|
|
@@ -55,10 +66,22 @@ export class EntityReference {
|
|
|
55
66
|
*/
|
|
56
67
|
readonly databaseId?: string;
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
/**
|
|
70
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
71
|
+
*
|
|
72
|
+
* `path` is a single flattened string, so a parent entity id containing "/" cannot be
|
|
73
|
+
* recovered from it. This keeps each segment whole.
|
|
74
|
+
*
|
|
75
|
+
* Optional, and never derived by splitting `path` — a guess would be wrong in exactly
|
|
76
|
+
* the case the field exists for. Absent means "not known here", not "no slashes".
|
|
77
|
+
*/
|
|
78
|
+
readonly pathSegments?: string[];
|
|
79
|
+
|
|
80
|
+
constructor(id: string, path: string, databaseId?: string, pathSegments?: string[]) {
|
|
59
81
|
this.id = id;
|
|
60
82
|
this.path = path;
|
|
61
83
|
this.databaseId = databaseId;
|
|
84
|
+
this.pathSegments = pathSegments;
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
get pathWithId() {
|
|
@@ -65,6 +65,12 @@ export type EntityActionClickProps<M extends object, USER extends User = User> =
|
|
|
65
65
|
context: FireCMSContext<USER>;
|
|
66
66
|
|
|
67
67
|
fullPath?: string;
|
|
68
|
+
/**
|
|
69
|
+
* `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`,
|
|
70
|
+
* when the caller knows them. Absent means "not known here" — never derived from
|
|
71
|
+
* `fullPath`, since a parent id may itself contain "/".
|
|
72
|
+
*/
|
|
73
|
+
pathSegments?: string[];
|
|
68
74
|
fullIdPath?: string;
|
|
69
75
|
collection?: EntityCollection<M>;
|
|
70
76
|
|
|
@@ -86,6 +86,14 @@ export interface EntityOnFetchProps<M extends Record<string, any> = any, USER ex
|
|
|
86
86
|
*/
|
|
87
87
|
path: string;
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
91
|
+
*
|
|
92
|
+
* Optional, and never derived by splitting `path` — a parent entity id may itself
|
|
93
|
+
* contain "/". Absent means "not known here", not "no slashes".
|
|
94
|
+
*/
|
|
95
|
+
pathSegments?: string[];
|
|
96
|
+
|
|
89
97
|
/**
|
|
90
98
|
* Fetched entity
|
|
91
99
|
*/
|
|
@@ -133,6 +141,14 @@ export interface EntityOnSaveProps<M extends Record<string, any> = any, USER ext
|
|
|
133
141
|
*/
|
|
134
142
|
path: string;
|
|
135
143
|
|
|
144
|
+
/**
|
|
145
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
146
|
+
*
|
|
147
|
+
* Optional, and never derived by splitting `path` — a parent entity id may itself
|
|
148
|
+
* contain "/". Absent means "not known here", not "no slashes".
|
|
149
|
+
*/
|
|
150
|
+
pathSegments?: string[];
|
|
151
|
+
|
|
136
152
|
/**
|
|
137
153
|
* Full path where this entity is being saved, with alias resolved
|
|
138
154
|
*/
|
|
@@ -180,6 +196,14 @@ export interface EntityOnDeleteProps<M extends Record<string, any> = any, USER e
|
|
|
180
196
|
*/
|
|
181
197
|
path: string;
|
|
182
198
|
|
|
199
|
+
/**
|
|
200
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
201
|
+
*
|
|
202
|
+
* Optional, and never derived by splitting `path` — a parent entity id may itself
|
|
203
|
+
* contain "/". Absent means "not known here", not "no slashes".
|
|
204
|
+
*/
|
|
205
|
+
pathSegments?: string[];
|
|
206
|
+
|
|
183
207
|
/**
|
|
184
208
|
* Deleted entity id
|
|
185
209
|
*/
|
package/src/types/navigation.ts
CHANGED
|
@@ -57,8 +57,12 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
|
|
|
57
57
|
/**
|
|
58
58
|
* Get the collection configuration for a given path.
|
|
59
59
|
* The collection is resolved from the given path or alias.
|
|
60
|
+
*
|
|
61
|
+
* Pass `pathSegments` when you have them: without them the path is split on "/" and
|
|
62
|
+
* asserted to have an odd number of parts, which a chain containing a slash-bearing
|
|
63
|
+
* entity id fails — it throws instead of resolving.
|
|
60
64
|
*/
|
|
61
|
-
getCollection: (pathOrId: string, includeUserOverride?: boolean) => EC | undefined;
|
|
65
|
+
getCollection: (pathOrId: string, includeUserOverride?: boolean, pathSegments?: string[]) => EC | undefined;
|
|
62
66
|
|
|
63
67
|
/**
|
|
64
68
|
* Get the top level collection configuration for a given id
|
|
@@ -118,9 +122,30 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
|
|
|
118
122
|
/**
|
|
119
123
|
* Turn a path with collection ids into a resolved path.
|
|
120
124
|
* The ids (typically used in urls) will be replaced with relative paths (typically used in database paths)
|
|
125
|
+
*
|
|
126
|
+
* Pass `pathSegments` whenever you have them. Without them this has to find the entity
|
|
127
|
+
* ids inside the flattened string by reading up to the next "/", which is wrong for any
|
|
128
|
+
* id that contains one — the chain then shifts by a segment and resolution fails.
|
|
129
|
+
*
|
|
121
130
|
* @param pathWithAliases
|
|
131
|
+
* @param pathSegments `pathWithAliases` split at its real segment boundaries, if known
|
|
122
132
|
*/
|
|
123
|
-
resolveIdsFrom: (pathWithAliases: string) => string;
|
|
133
|
+
resolveIdsFrom: (pathWithAliases: string, pathSegments?: string[]) => string;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The segment-wise counterpart of {@link resolveIdsFrom}: replaces collection ids with
|
|
137
|
+
* their real paths while carrying every entity id through whole, slashes included.
|
|
138
|
+
*
|
|
139
|
+
* Idempotent — segments that are already resolved pass through unchanged.
|
|
140
|
+
*
|
|
141
|
+
* Optional so that a `navigationController` built against an earlier version keeps
|
|
142
|
+
* working: callers fall back to using the segments as given, which is what happened
|
|
143
|
+
* before this method existed. Controllers built by `useBuildNavigationController`
|
|
144
|
+
* always provide it.
|
|
145
|
+
*
|
|
146
|
+
* @param pathSegments
|
|
147
|
+
*/
|
|
148
|
+
resolveSegmentsFrom?: (pathSegments: string[]) => string[];
|
|
124
149
|
|
|
125
150
|
/**
|
|
126
151
|
* Call this method to recalculate the navigation
|
|
@@ -128,16 +153,21 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
|
|
|
128
153
|
refreshNavigation: () => void;
|
|
129
154
|
|
|
130
155
|
/**
|
|
131
|
-
* Retrieve all the related parent references for a given path
|
|
156
|
+
* Retrieve all the related parent references for a given path.
|
|
157
|
+
*
|
|
158
|
+
* `path` is expected escaped, as it comes from the URL. Pass `pathSegments` when the
|
|
159
|
+
* path at hand is a raw datasource path, whose ids may contain a bare "/".
|
|
160
|
+
*
|
|
132
161
|
* @param path
|
|
162
|
+
* @param pathSegments `path` split at its real segment boundaries, with raw ids
|
|
133
163
|
*/
|
|
134
|
-
getParentReferencesFromPath: (path: string) => EntityReference[];
|
|
164
|
+
getParentReferencesFromPath: (path: string, pathSegments?: string[]) => EntityReference[];
|
|
135
165
|
|
|
136
166
|
/**
|
|
137
167
|
* Retrieve all the related parent collection ids for a given path
|
|
138
168
|
* @param path
|
|
139
169
|
*/
|
|
140
|
-
getParentCollectionIds: (path: string) => string[];
|
|
170
|
+
getParentCollectionIds: (path: string, pathSegments?: string[]) => string[];
|
|
141
171
|
|
|
142
172
|
/**
|
|
143
173
|
* Resolve paths from a list of ids
|
package/src/types/plugins.tsx
CHANGED
|
@@ -146,6 +146,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
|
|
|
146
146
|
blockSearch?: (props: {
|
|
147
147
|
context: FireCMSContext,
|
|
148
148
|
path: string,
|
|
149
|
+
/** `path` split at its real segment boundaries, when known. */
|
|
150
|
+
pathSegments?: string[],
|
|
149
151
|
collection: EC,
|
|
150
152
|
parentCollectionIds?: string[]
|
|
151
153
|
}) => boolean;
|
|
@@ -153,6 +155,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
|
|
|
153
155
|
showTextSearchBar?: (props: {
|
|
154
156
|
context: FireCMSContext,
|
|
155
157
|
path: string,
|
|
158
|
+
/** `path` split at its real segment boundaries, when known. */
|
|
159
|
+
pathSegments?: string[],
|
|
156
160
|
collection: EC,
|
|
157
161
|
parentCollectionIds?: string[]
|
|
158
162
|
}) => boolean;
|
|
@@ -160,6 +164,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
|
|
|
160
164
|
onTextSearchClick?: (props: {
|
|
161
165
|
context: FireCMSContext,
|
|
162
166
|
path: string,
|
|
167
|
+
/** `path` split at its real segment boundaries, when known. */
|
|
168
|
+
pathSegments?: string[],
|
|
163
169
|
collection: EC,
|
|
164
170
|
parentCollectionIds?: string[]
|
|
165
171
|
}) => Promise<boolean>;
|
|
@@ -308,6 +314,13 @@ export interface PluginHomePageActionsProps<EP extends object = object, M extend
|
|
|
308
314
|
export interface PluginFormActionProps<USER extends User = User, EC extends EntityCollection = EntityCollection> {
|
|
309
315
|
entityId?: string;
|
|
310
316
|
path: string;
|
|
317
|
+
/**
|
|
318
|
+
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
319
|
+
*
|
|
320
|
+
* Optional, and never derived by splitting `path` — a parent entity id may itself
|
|
321
|
+
* contain "/". Absent means "not known here", not "no slashes".
|
|
322
|
+
*/
|
|
323
|
+
pathSegments?: string[];
|
|
311
324
|
parentCollectionIds: string[];
|
|
312
325
|
status: EntityStatus;
|
|
313
326
|
collection: EC;
|
|
@@ -21,8 +21,9 @@ export interface EntitySidePanelProps<M extends Record<string, any> = any> {
|
|
|
21
21
|
fullIdPath?: string;
|
|
22
22
|
/**
|
|
23
23
|
* `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
*
|
|
25
|
+
* Optional, and never derived by splitting `path` — a guess would be wrong in exactly
|
|
26
|
+
* the case the field exists for. Absent means "not known here", not "no slashes".
|
|
26
27
|
*/
|
|
27
28
|
pathSegments?: string[];
|
|
28
29
|
|
package/src/util/entities.ts
CHANGED
|
@@ -142,7 +142,9 @@ export function getReferenceFrom<M extends Record<string, any>>(entity: Entity<M
|
|
|
142
142
|
if (!entity) {
|
|
143
143
|
throw new Error("getReferenceFrom: entity is null or undefined");
|
|
144
144
|
}
|
|
145
|
-
|
|
145
|
+
// Carry the segments the entity was loaded with, so a reference to an entity under a
|
|
146
|
+
// slash-bearing parent stays resolvable. Undefined when the entity does not have them.
|
|
147
|
+
return new EntityReference(entity.id, entity.path, entity.databaseId, entity.pathSegments);
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
export function traverseValuesProperties<M extends Record<string, any>>(
|
package/src/util/entity_cache.ts
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import { EntityReference, GeoPoint, Vector } from "../types";
|
|
2
2
|
import { isObject, isPlainObject } from "./objects";
|
|
3
|
+
import { encodeEntityId } from "./navigation_utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Key identifying one entity in a cache.
|
|
7
|
+
*
|
|
8
|
+
* The id is escaped so that a `(path, entityId)` pair maps to exactly one key. Without it
|
|
9
|
+
* `("a", "b/c")` and `("a/b", "c")` both flatten to `"a/b/c"`, and one entity's cached
|
|
10
|
+
* values are served for the other.
|
|
11
|
+
*
|
|
12
|
+
* `encodeEntityId` is the identity for any id without "/", "?", "#" or "%", so this changes
|
|
13
|
+
* no key in an app whose ids cannot contain them.
|
|
14
|
+
*/
|
|
15
|
+
export function entityCacheKey(path: string | undefined, entityId: string | undefined): string {
|
|
16
|
+
// Either part may be undefined at a call site that used to build the key by string
|
|
17
|
+
// concatenation; both are stringified exactly as `path + "/" + entityId` did, so such a
|
|
18
|
+
// caller keeps the key it had.
|
|
19
|
+
return `${path}/${entityId === undefined ? entityId : encodeEntityId(entityId)}`;
|
|
20
|
+
}
|
|
3
21
|
|
|
4
22
|
// Define a unique prefix for entity keys in localStorage to avoid key collisions
|
|
5
23
|
const LOCAL_STORAGE_PREFIX = "entity_cache::";
|
|
@@ -147,7 +147,12 @@ export function getNavigationEntriesFromPath(props: {
|
|
|
147
147
|
path: newPath,
|
|
148
148
|
collections: collection.subcollections,
|
|
149
149
|
currentFullPath: fullPath,
|
|
150
|
-
|
|
150
|
+
// The entity id is a hop in the id chain exactly as it is in the
|
|
151
|
+
// other two. Without it a nested `fullIdPath` was
|
|
152
|
+
// "products/locales" rather than "products/pid/locales", so any
|
|
153
|
+
// URL built from it pointed at a collection that does not exist.
|
|
154
|
+
// Escaped, because `fullIdPath` is URL-facing.
|
|
155
|
+
currentFullIdPath: fullIdPath + "/" + encodedEntityId,
|
|
151
156
|
currentFullUrlPath: fullUrlPath,
|
|
152
157
|
currentPathSegments: entitySegments,
|
|
153
158
|
contextEntityViews: props.contextEntityViews
|
|
@@ -56,6 +56,30 @@ export function decodeEntityId(encodedEntityId: string): string {
|
|
|
56
56
|
.replaceAll("%25", "%");
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Segments of a subcollection sitting under the entity `entityId` of `parentPathSegments`.
|
|
61
|
+
*
|
|
62
|
+
* The parent entity id is kept whole however many slashes it contains, while the
|
|
63
|
+
* subcollection's own configured path is spread — a collection path never contains an
|
|
64
|
+
* entity id, so splitting that one is unambiguous.
|
|
65
|
+
*
|
|
66
|
+
* So `["test"] + "test/test" + "accommodation"` yields three segments,
|
|
67
|
+
* `["test", "test/test", "accommodation"]`, where splitting the flattened path
|
|
68
|
+
* `"test/test/test/accommodation"` would wrongly yield four.
|
|
69
|
+
*
|
|
70
|
+
* Returns undefined when the parent segments are unknown: the parent chain cannot be
|
|
71
|
+
* recovered from a flattened path, so there is nothing honest to build on.
|
|
72
|
+
*
|
|
73
|
+
* @group Hooks and utilities
|
|
74
|
+
*/
|
|
75
|
+
export function buildSubcollectionPathSegments(parentPathSegments: string[] | undefined,
|
|
76
|
+
entityId: string | undefined,
|
|
77
|
+
subcollectionPath: string): string[] | undefined {
|
|
78
|
+
if (!parentPathSegments || !entityId) return undefined;
|
|
79
|
+
const ownSegments = removeInitialAndTrailingSlashes(subcollectionPath).split("/");
|
|
80
|
+
return [...parentPathSegments, entityId, ...ownSegments];
|
|
81
|
+
}
|
|
82
|
+
|
|
59
83
|
export function getLastSegment(path: string) {
|
|
60
84
|
const cleanPath = removeInitialAndTrailingSlashes(path);
|
|
61
85
|
if (cleanPath.includes("/")) {
|
|
@@ -65,7 +89,171 @@ export function getLastSegment(path: string) {
|
|
|
65
89
|
return cleanPath;
|
|
66
90
|
}
|
|
67
91
|
|
|
68
|
-
|
|
92
|
+
/**
|
|
93
|
+
* The outcome of walking a segment chain against a collection tree.
|
|
94
|
+
* @group Hooks and utilities
|
|
95
|
+
*/
|
|
96
|
+
export type PathSegmentsWalkStep = {
|
|
97
|
+
collection: EntityCollection;
|
|
98
|
+
/** Resolved path of this collection, e.g. `"products/pid/locales"`. */
|
|
99
|
+
collectionPath: string;
|
|
100
|
+
/** `collectionPath` split at its real segment boundaries. */
|
|
101
|
+
collectionSegments: string[];
|
|
102
|
+
/** The entity addressed inside it, when the chain continues past the collection. */
|
|
103
|
+
entityId?: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type PathSegmentsWalk = {
|
|
107
|
+
/** Segments with every collection id replaced by the collection's real `path`. */
|
|
108
|
+
resolved: string[];
|
|
109
|
+
/** The collection the chain ends in, if the whole chain matched. */
|
|
110
|
+
collection?: EntityCollection;
|
|
111
|
+
/** One entry per collection level traversed, outermost first. */
|
|
112
|
+
steps: PathSegmentsWalkStep[];
|
|
113
|
+
/** The first segment that could not be matched, if the walk stopped early. */
|
|
114
|
+
unmatched?: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Walk a segment chain against a collection tree, matching a collection by either its
|
|
119
|
+
* `path` or its `id` and treating exactly one segment as each entity id.
|
|
120
|
+
*
|
|
121
|
+
* This is the primitive the string-based helpers cannot have: working on the flattened
|
|
122
|
+
* path they must *guess* where an entity id ends by reading up to the next "/", which is
|
|
123
|
+
* wrong for any id that contains one. Here the boundaries are given.
|
|
124
|
+
*
|
|
125
|
+
* Stops at the first segment it cannot match and reports it, rather than throwing.
|
|
126
|
+
*
|
|
127
|
+
* @group Hooks and utilities
|
|
128
|
+
*/
|
|
129
|
+
export function walkPathSegments(pathSegments: string[], allCollections: EntityCollection[]): PathSegmentsWalk {
|
|
130
|
+
|
|
131
|
+
const resolved: string[] = [];
|
|
132
|
+
const steps: PathSegmentsWalkStep[] = [];
|
|
133
|
+
let currentCollections: EntityCollection[] | undefined = allCollections;
|
|
134
|
+
let index = 0;
|
|
135
|
+
|
|
136
|
+
while (index < pathSegments.length) {
|
|
137
|
+
|
|
138
|
+
const remaining = pathSegments.slice(index);
|
|
139
|
+
|
|
140
|
+
if (!currentCollections || currentCollections.length === 0) {
|
|
141
|
+
resolved.push(...remaining);
|
|
142
|
+
return {
|
|
143
|
+
resolved,
|
|
144
|
+
steps,
|
|
145
|
+
unmatched: remaining[0]
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A collection's own `path` may span several segments ("users/uid/experiences"), so
|
|
150
|
+
// candidates are matched segment by segment and the longest match wins.
|
|
151
|
+
let match: { collection: EntityCollection, length: number } | undefined;
|
|
152
|
+
for (const collection of currentCollections) {
|
|
153
|
+
for (const candidate of [collection.path, collection.id]) {
|
|
154
|
+
if (!candidate) continue;
|
|
155
|
+
const parts = removeInitialAndTrailingSlashes(candidate).split("/");
|
|
156
|
+
if (parts.length > remaining.length) continue;
|
|
157
|
+
if (parts.some((part, i) => part !== remaining[i])) continue;
|
|
158
|
+
if (!match || parts.length > match.length) match = { collection, length: parts.length };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!match) {
|
|
163
|
+
resolved.push(...remaining);
|
|
164
|
+
return {
|
|
165
|
+
resolved,
|
|
166
|
+
steps,
|
|
167
|
+
unmatched: remaining[0]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
resolved.push(...removeInitialAndTrailingSlashes(match.collection.path).split("/"));
|
|
172
|
+
index += match.length;
|
|
173
|
+
|
|
174
|
+
const step: PathSegmentsWalkStep = {
|
|
175
|
+
collection: match.collection,
|
|
176
|
+
collectionPath: resolved.join("/"),
|
|
177
|
+
collectionSegments: [...resolved]
|
|
178
|
+
};
|
|
179
|
+
steps.push(step);
|
|
180
|
+
|
|
181
|
+
// The chain ends on a collection, which is the one being addressed.
|
|
182
|
+
if (index >= pathSegments.length) {
|
|
183
|
+
return {
|
|
184
|
+
resolved,
|
|
185
|
+
steps,
|
|
186
|
+
collection: match.collection
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Exactly one segment is the entity id, however many slashes it contains.
|
|
191
|
+
step.entityId = pathSegments[index];
|
|
192
|
+
resolved.push(pathSegments[index]);
|
|
193
|
+
index += 1;
|
|
194
|
+
|
|
195
|
+
// The chain ends on an entity, which lives in the collection just matched.
|
|
196
|
+
if (index >= pathSegments.length) {
|
|
197
|
+
return {
|
|
198
|
+
resolved,
|
|
199
|
+
steps,
|
|
200
|
+
collection: match.collection
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
currentCollections = match.collection.subcollections;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { resolved, steps };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolve collection aliases in a segment array: every collection id is replaced by the
|
|
212
|
+
* collection's real `path`, and every entity id is carried through untouched.
|
|
213
|
+
*
|
|
214
|
+
* The segment-wise counterpart of {@link resolveCollectionPathIds}, and the reason it
|
|
215
|
+
* exists: that one works on the flattened string, so an entity id containing "/" shifts
|
|
216
|
+
* the rest of the chain by a segment and resolution fails with
|
|
217
|
+
* "Collection definition not found for segment starting with …".
|
|
218
|
+
*
|
|
219
|
+
* Matching accepts either a collection's `path` or its `id`, so already-resolved segments
|
|
220
|
+
* pass through unchanged — the function is idempotent.
|
|
221
|
+
*
|
|
222
|
+
* Mirrors the string version when a chain cannot be matched: it warns and carries the
|
|
223
|
+
* remainder through unresolved, rather than throwing.
|
|
224
|
+
*
|
|
225
|
+
* @group Hooks and utilities
|
|
226
|
+
*/
|
|
227
|
+
export function resolveCollectionPathSegments(pathSegments: string[], allCollections: EntityCollection[]): string[] {
|
|
228
|
+
const walk = walkPathSegments(pathSegments, allCollections);
|
|
229
|
+
if (walk.unmatched !== undefined) {
|
|
230
|
+
console.warn(`resolveCollectionPathSegments: Collection definition not found for segment "${walk.unmatched}" in [${pathSegments.join(", ")}]. Carrying the remaining segments through unresolved.`);
|
|
231
|
+
}
|
|
232
|
+
return walk.resolved;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Find the collection a segment chain addresses, whether it ends on the collection itself
|
|
237
|
+
* or on one of its entities.
|
|
238
|
+
*
|
|
239
|
+
* The segment-wise counterpart of {@link getCollectionByPathOrId}, which asserts an odd
|
|
240
|
+
* number of "/"-separated parts — an assertion a slash-bearing entity id fails, throwing
|
|
241
|
+
* where it should simply have resolved.
|
|
242
|
+
*
|
|
243
|
+
* @group Hooks and utilities
|
|
244
|
+
*/
|
|
245
|
+
export function getCollectionByPathSegments(pathSegments: string[], collections: EntityCollection[]): EntityCollection | undefined {
|
|
246
|
+
return walkPathSegments(pathSegments, collections).collection;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function resolveCollectionPathIds(path: string, allCollections: EntityCollection[], pathSegments?: string[]): string {
|
|
250
|
+
|
|
251
|
+
// When the caller knows the real segment boundaries there is nothing to parse: the
|
|
252
|
+
// ambiguity the string walk below has to guess its way through simply is not present.
|
|
253
|
+
if (pathSegments) {
|
|
254
|
+
return resolveCollectionPathSegments(pathSegments, allCollections).join("/");
|
|
255
|
+
}
|
|
256
|
+
|
|
69
257
|
let remainingPath = removeInitialAndTrailingSlashes(path);
|
|
70
258
|
if (!remainingPath) {
|
|
71
259
|
return "";
|
|
@@ -158,7 +346,14 @@ export function resolveCollectionPathIds(path: string, allCollections: EntityCol
|
|
|
158
346
|
* @param pathOrId
|
|
159
347
|
* @param collections
|
|
160
348
|
*/
|
|
161
|
-
export function getCollectionByPathOrId(pathOrId: string, collections: EntityCollection[]): EntityCollection | undefined {
|
|
349
|
+
export function getCollectionByPathOrId(pathOrId: string, collections: EntityCollection[], pathSegments?: string[]): EntityCollection | undefined {
|
|
350
|
+
|
|
351
|
+
// Given the real boundaries there is nothing to parse, and no parity to assert: a chain
|
|
352
|
+
// whose entity id contains "/" splits into an even number of parts below and would be
|
|
353
|
+
// rejected outright, even though it is perfectly valid.
|
|
354
|
+
if (pathSegments) {
|
|
355
|
+
return getCollectionByPathSegments(pathSegments, collections);
|
|
356
|
+
}
|
|
162
357
|
|
|
163
358
|
const subpaths = removeInitialAndTrailingSlashes(pathOrId).split("/");
|
|
164
359
|
if (subpaths.length % 2 === 0) {
|
|
@@ -249,6 +444,13 @@ export function navigateToEntity({
|
|
|
249
444
|
});
|
|
250
445
|
|
|
251
446
|
} else {
|
|
447
|
+
// A URL must be built from the ESCAPED chain. `fullIdPath` is that chain; `path` is
|
|
448
|
+
// the raw datasource one, and falling back to it is only safe while no id in it
|
|
449
|
+
// contains "/". When the segments show that it does, the resulting URL would address
|
|
450
|
+
// a different entity, so say so instead of navigating somewhere wrong in silence.
|
|
451
|
+
if (!fullIdPath && pathSegments?.some(segment => segment.includes("/"))) {
|
|
452
|
+
console.warn(`navigateToEntity: no "fullIdPath" was given and "${path}" contains an entity id with a "/", so no unambiguous URL can be built for it. The link will point at a different location. Pass "fullIdPath" — the escaped chain — alongside "path".`);
|
|
453
|
+
}
|
|
252
454
|
let to = navigation.buildUrlCollectionPath(entityId ? `${fullIdPath ?? path}/${encodeEntityId(entityId)}` : fullIdPath ?? path);
|
|
253
455
|
if (entityId && selectedTab) {
|
|
254
456
|
to += `/${selectedTab}`;
|