@firecms/core 3.4.0-canary.63041d0 → 3.4.0-canary.cb27f16

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.
Files changed (82) hide show
  1. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  2. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  3. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +3 -2
  4. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  5. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  7. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  8. package/dist/components/common/useDataSourceTableController.d.ts +4 -2
  9. package/dist/components/common/useTableSearchHelper.d.ts +3 -1
  10. package/dist/core/EntityEditView.d.ts +2 -0
  11. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  12. package/dist/form/EntityForm.d.ts +8 -1
  13. package/dist/form/EntityFormActions.d.ts +2 -0
  14. package/dist/hooks/data/delete.d.ts +1 -1
  15. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  16. package/dist/hooks/data/useEntityFetch.d.ts +4 -1
  17. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  18. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  19. package/dist/index.es.js +970 -652
  20. package/dist/index.es.js.map +1 -1
  21. package/dist/index.umd.js +969 -651
  22. package/dist/index.umd.js.map +1 -1
  23. package/dist/types/collections.d.ts +7 -0
  24. package/dist/types/datasource.d.ts +25 -4
  25. package/dist/types/entities.d.ts +21 -1
  26. package/dist/types/entity_actions.d.ts +6 -0
  27. package/dist/types/entity_callbacks.d.ts +21 -0
  28. package/dist/types/navigation.d.ts +34 -5
  29. package/dist/types/plugins.d.ts +13 -0
  30. package/dist/types/side_entity_controller.d.ts +3 -2
  31. package/dist/util/entity_cache.d.ts +11 -0
  32. package/dist/util/navigation_utils.d.ts +84 -2
  33. package/dist/util/parent_references_from_path.d.ts +16 -0
  34. package/dist/util/paths.d.ts +28 -0
  35. package/dist/util/permissions.d.ts +10 -4
  36. package/package.json +4 -4
  37. package/src/components/DeleteEntityDialog.tsx +2 -0
  38. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  39. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  40. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  41. package/src/components/EntityCollectionView/EntityCollectionView.tsx +49 -16
  42. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  43. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
  44. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  45. package/src/components/EntityPreview.tsx +4 -1
  46. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  47. package/src/components/common/default_entity_actions.tsx +22 -2
  48. package/src/components/common/useDataSourceTableController.tsx +32 -8
  49. package/src/components/common/useTableSearchHelper.ts +5 -0
  50. package/src/core/EntityEditView.tsx +11 -12
  51. package/src/core/EntityEditViewFormActions.tsx +3 -2
  52. package/src/core/EntitySidePanel.tsx +6 -4
  53. package/src/form/EntityForm.tsx +30 -9
  54. package/src/form/EntityFormActions.tsx +2 -0
  55. package/src/hooks/data/delete.ts +8 -0
  56. package/src/hooks/data/save.ts +5 -2
  57. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  58. package/src/hooks/data/useEntityFetch.tsx +28 -6
  59. package/src/hooks/useBuildNavigationController.tsx +23 -8
  60. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  61. package/src/i18n/FireCMSi18nProvider.tsx +48 -4
  62. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  63. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  64. package/src/internal/useBuildDataSource.ts +26 -15
  65. package/src/internal/useBuildSideEntityController.tsx +13 -5
  66. package/src/preview/components/ReferencePreview.tsx +7 -3
  67. package/src/routes/FireCMSRoute.tsx +9 -4
  68. package/src/types/collections.ts +8 -0
  69. package/src/types/datasource.ts +25 -4
  70. package/src/types/entities.ts +24 -1
  71. package/src/types/entity_actions.tsx +6 -0
  72. package/src/types/entity_callbacks.ts +24 -0
  73. package/src/types/navigation.ts +35 -5
  74. package/src/types/plugins.tsx +13 -0
  75. package/src/types/side_entity_controller.tsx +3 -2
  76. package/src/util/entities.ts +3 -1
  77. package/src/util/entity_cache.ts +18 -0
  78. package/src/util/navigation_from_path.ts +6 -1
  79. package/src/util/navigation_utils.ts +204 -2
  80. package/src/util/parent_references_from_path.ts +42 -3
  81. package/src/util/paths.ts +40 -3
  82. package/src/util/permissions.ts +22 -10
@@ -439,6 +439,13 @@ export interface CollectionActionsProps<M extends Record<string, any> = any, USE
439
439
  * `users/1234/addresses`
440
440
  */
441
441
  path: string;
442
+ /**
443
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
444
+ *
445
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
446
+ * contain "/". Absent means "not known here", not "no slashes".
447
+ */
448
+ pathSegments?: string[];
442
449
  /**
443
450
  * Path of the last collection, like `addresses`
444
451
  */
@@ -96,6 +96,11 @@ export interface SaveEntityProps<M extends Record<string, any> = any> {
96
96
  */
97
97
  export interface DeleteEntityProps<M extends Record<string, any> = any> {
98
98
  entity: Entity<M>;
99
+ /**
100
+ * `entity.path` split at its real segment boundaries, when the caller knows them.
101
+ * Never derived from `entity.path` — see `pathSegments` on FetchCollectionProps.
102
+ */
103
+ pathSegments?: string[];
99
104
  collection?: EntityCollection<M> | ResolvedEntityCollection<M>;
100
105
  }
101
106
  /**
@@ -175,11 +180,17 @@ export interface DataSource {
175
180
  * @param entityId
176
181
  * @return `true` if there are no other fields besides the given entity
177
182
  */
178
- checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection): Promise<boolean>;
183
+ checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection,
184
+ /**
185
+ * Appended last, and optional, so existing implementations and callers keep
186
+ * working unchanged — a function declaring fewer parameters stays assignable.
187
+ * See `pathSegments` on FetchCollectionProps.
188
+ */
189
+ pathSegments?: string[]): Promise<boolean>;
179
190
  /**
180
191
  * Generate an id for a new entity
181
192
  */
182
- generateEntityId(path: string, collection: EntityCollection): string;
193
+ generateEntityId(path: string, collection: EntityCollection, pathSegments?: string[]): string;
183
194
  /**
184
195
  * Count the number of entities in a collection
185
196
  */
@@ -197,11 +208,17 @@ export interface DataSource {
197
208
  initTextSearch?: (props: {
198
209
  context: FireCMSContext;
199
210
  path: string;
211
+ /**
212
+ * `path` split at its real segment boundaries, when known. Never derived from `path`.
213
+ */
214
+ pathSegments?: string[];
200
215
  collection: EntityCollection;
201
216
  parentCollectionIds?: string[];
202
217
  }) => Promise<boolean>;
203
218
  }
204
219
  export type FilterCombinationValidProps = {
220
+ /** `path` split at its real segment boundaries — see `pathSegments` on FetchCollectionProps. */
221
+ pathSegments?: string[];
205
222
  path: string;
206
223
  collection: EntityCollection<any>;
207
224
  filterValues: FilterValues<any>;
@@ -287,11 +304,11 @@ export interface DataSourceDelegate {
287
304
  * @param collection
288
305
  * @return `true` if there are no other fields besides the given entity
289
306
  */
290
- checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection): Promise<boolean>;
307
+ checkUniqueField(path: string, name: string, value: any, entityId?: string, collection?: EntityCollection, pathSegments?: string[]): Promise<boolean>;
291
308
  /**
292
309
  * Generate an id for a new entity
293
310
  */
294
- generateEntityId(path: string, collection?: EntityCollection): string;
311
+ generateEntityId(path: string, collection?: EntityCollection, pathSegments?: string[]): string;
295
312
  /**
296
313
  * Count the number of entities in a collection
297
314
  */
@@ -312,6 +329,10 @@ export interface DataSourceDelegate {
312
329
  initTextSearch?: (props: {
313
330
  context: FireCMSContext;
314
331
  path: string;
332
+ /**
333
+ * `path` split at its real segment boundaries, when known. Never derived from `path`.
334
+ */
335
+ pathSegments?: string[];
315
336
  databaseId?: string;
316
337
  collection: EntityCollection;
317
338
  parentCollectionIds?: string[];
@@ -17,6 +17,16 @@ export interface Entity<M extends object = any> {
17
17
  * to the root of the database).
18
18
  */
19
19
  path: string;
20
+ /**
21
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
22
+ *
23
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
24
+ * recovered from it. This keeps each segment whole.
25
+ *
26
+ * Optional, and never derived by splitting `path` — a guess would be wrong in exactly
27
+ * the case the field exists for. Absent means "not known here", not "no slashes".
28
+ */
29
+ pathSegments?: string[];
20
30
  /**
21
31
  * Current values
22
32
  */
@@ -46,7 +56,17 @@ export declare class EntityReference {
46
56
  * Optional database ID where the entity is stored (if multiple databases are used)
47
57
  */
48
58
  readonly databaseId?: string;
49
- constructor(id: string, path: string, databaseId?: string);
59
+ /**
60
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
61
+ *
62
+ * `path` is a single flattened string, so a parent entity id containing "/" cannot be
63
+ * recovered from it. This keeps each segment whole.
64
+ *
65
+ * Optional, and never derived by splitting `path` — a guess would be wrong in exactly
66
+ * the case the field exists for. Absent means "not known here", not "no slashes".
67
+ */
68
+ readonly pathSegments?: string[];
69
+ constructor(id: string, path: string, databaseId?: string, pathSegments?: string[]);
50
70
  get pathWithId(): string;
51
71
  get pathWithIdAndDatabase(): string;
52
72
  isEntityReference(): boolean;
@@ -55,6 +55,12 @@ export type EntityActionClickProps<M extends object, USER extends User = User> =
55
55
  entity?: Entity<M>;
56
56
  context: FireCMSContext<USER>;
57
57
  fullPath?: string;
58
+ /**
59
+ * `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`,
60
+ * when the caller knows them. Absent means "not known here" — never derived from
61
+ * `fullPath`, since a parent id may itself contain "/".
62
+ */
63
+ pathSegments?: string[];
58
64
  fullIdPath?: string;
59
65
  collection?: EntityCollection<M>;
60
66
  /**
@@ -69,6 +69,13 @@ export interface EntityOnFetchProps<M extends Record<string, any> = any, USER ex
69
69
  * Might contain unresolved aliases.
70
70
  */
71
71
  path: string;
72
+ /**
73
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
74
+ *
75
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
76
+ * contain "/". Absent means "not known here", not "no slashes".
77
+ */
78
+ pathSegments?: string[];
72
79
  /**
73
80
  * Fetched entity
74
81
  */
@@ -106,6 +113,13 @@ export interface EntityOnSaveProps<M extends Record<string, any> = any, USER ext
106
113
  * Might contain unresolved aliases.
107
114
  */
108
115
  path: string;
116
+ /**
117
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
118
+ *
119
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
120
+ * contain "/". Absent means "not known here", not "no slashes".
121
+ */
122
+ pathSegments?: string[];
109
123
  /**
110
124
  * Full path where this entity is being saved, with alias resolved
111
125
  */
@@ -144,6 +158,13 @@ export interface EntityOnDeleteProps<M extends Record<string, any> = any, USER e
144
158
  * Path of the parent collection
145
159
  */
146
160
  path: string;
161
+ /**
162
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
163
+ *
164
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
165
+ * contain "/". Absent means "not known here", not "no slashes".
166
+ */
167
+ pathSegments?: string[];
147
168
  /**
148
169
  * Deleted entity id
149
170
  */
@@ -48,8 +48,12 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
48
48
  /**
49
49
  * Get the collection configuration for a given path.
50
50
  * The collection is resolved from the given path or alias.
51
+ *
52
+ * Pass `pathSegments` when you have them: without them the path is split on "/" and
53
+ * asserted to have an odd number of parts, which a chain containing a slash-bearing
54
+ * entity id fails — it throws instead of resolving.
51
55
  */
52
- getCollection: (pathOrId: string, includeUserOverride?: boolean) => EC | undefined;
56
+ getCollection: (pathOrId: string, includeUserOverride?: boolean, pathSegments?: string[]) => EC | undefined;
53
57
  /**
54
58
  * Get the top level collection configuration for a given id
55
59
  */
@@ -99,23 +103,48 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
99
103
  /**
100
104
  * Turn a path with collection ids into a resolved path.
101
105
  * The ids (typically used in urls) will be replaced with relative paths (typically used in database paths)
106
+ *
107
+ * Pass `pathSegments` whenever you have them. Without them this has to find the entity
108
+ * ids inside the flattened string by reading up to the next "/", which is wrong for any
109
+ * id that contains one — the chain then shifts by a segment and resolution fails.
110
+ *
102
111
  * @param pathWithAliases
112
+ * @param pathSegments `pathWithAliases` split at its real segment boundaries, if known
113
+ */
114
+ resolveIdsFrom: (pathWithAliases: string, pathSegments?: string[]) => string;
115
+ /**
116
+ * The segment-wise counterpart of {@link resolveIdsFrom}: replaces collection ids with
117
+ * their real paths while carrying every entity id through whole, slashes included.
118
+ *
119
+ * Idempotent — segments that are already resolved pass through unchanged.
120
+ *
121
+ * Optional so that a `navigationController` built against an earlier version keeps
122
+ * working: callers fall back to using the segments as given, which is what happened
123
+ * before this method existed. Controllers built by `useBuildNavigationController`
124
+ * always provide it.
125
+ *
126
+ * @param pathSegments
103
127
  */
104
- resolveIdsFrom: (pathWithAliases: string) => string;
128
+ resolveSegmentsFrom?: (pathSegments: string[]) => string[];
105
129
  /**
106
130
  * Call this method to recalculate the navigation
107
131
  */
108
132
  refreshNavigation: () => void;
109
133
  /**
110
- * Retrieve all the related parent references for a given path
134
+ * Retrieve all the related parent references for a given path.
135
+ *
136
+ * `path` is expected escaped, as it comes from the URL. Pass `pathSegments` when the
137
+ * path at hand is a raw datasource path, whose ids may contain a bare "/".
138
+ *
111
139
  * @param path
140
+ * @param pathSegments `path` split at its real segment boundaries, with raw ids
112
141
  */
113
- getParentReferencesFromPath: (path: string) => EntityReference[];
142
+ getParentReferencesFromPath: (path: string, pathSegments?: string[]) => EntityReference[];
114
143
  /**
115
144
  * Retrieve all the related parent collection ids for a given path
116
145
  * @param path
117
146
  */
118
- getParentCollectionIds: (path: string) => string[];
147
+ getParentCollectionIds: (path: string, pathSegments?: string[]) => string[];
119
148
  /**
120
149
  * Resolve paths from a list of ids
121
150
  * @param ids
@@ -121,18 +121,24 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
121
121
  blockSearch?: (props: {
122
122
  context: FireCMSContext;
123
123
  path: string;
124
+ /** `path` split at its real segment boundaries, when known. */
125
+ pathSegments?: string[];
124
126
  collection: EC;
125
127
  parentCollectionIds?: string[];
126
128
  }) => boolean;
127
129
  showTextSearchBar?: (props: {
128
130
  context: FireCMSContext;
129
131
  path: string;
132
+ /** `path` split at its real segment boundaries, when known. */
133
+ pathSegments?: string[];
130
134
  collection: EC;
131
135
  parentCollectionIds?: string[];
132
136
  }) => boolean;
133
137
  onTextSearchClick?: (props: {
134
138
  context: FireCMSContext;
135
139
  path: string;
140
+ /** `path` split at its real segment boundaries, when known. */
141
+ pathSegments?: string[];
136
142
  collection: EC;
137
143
  parentCollectionIds?: string[];
138
144
  }) => Promise<boolean>;
@@ -258,6 +264,13 @@ export interface PluginHomePageActionsProps<EP extends object = object, M extend
258
264
  export interface PluginFormActionProps<USER extends User = User, EC extends EntityCollection = EntityCollection> {
259
265
  entityId?: string;
260
266
  path: string;
267
+ /**
268
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
269
+ *
270
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
271
+ * contain "/". Absent means "not known here", not "no slashes".
272
+ */
273
+ pathSegments?: string[];
261
274
  parentCollectionIds: string[];
262
275
  status: EntityStatus;
263
276
  collection: EC;
@@ -17,8 +17,9 @@ export interface EntitySidePanelProps<M extends Record<string, any> = any> {
17
17
  fullIdPath?: string;
18
18
  /**
19
19
  * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
20
- * Optional: when omitted the panel falls back to splitting `path`, which is correct for
21
- * any backend whose entity ids cannot contain "/".
20
+ *
21
+ * Optional, and never derived by splitting `path` — a guess would be wrong in exactly
22
+ * the case the field exists for. Absent means "not known here", not "no slashes".
22
23
  */
23
24
  pathSegments?: string[];
24
25
  /**
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Key identifying one entity in a cache.
3
+ *
4
+ * The id is escaped so that a `(path, entityId)` pair maps to exactly one key. Without it
5
+ * `("a", "b/c")` and `("a/b", "c")` both flatten to `"a/b/c"`, and one entity's cached
6
+ * values are served for the other.
7
+ *
8
+ * `encodeEntityId` is the identity for any id without "/", "?", "#" or "%", so this changes
9
+ * no key in an app whose ids cannot contain them.
10
+ */
11
+ export declare function entityCacheKey(path: string | undefined, entityId: string | undefined): string;
1
12
  /**
2
13
  * Saves data to the in-memory cache and persists it individually in `localStorage`.
3
14
  * @param path - The unique path/key for the data.
@@ -23,15 +23,97 @@ export declare function encodeEntityId(entityId: string): string;
23
23
  * @group Hooks and utilities
24
24
  */
25
25
  export declare function decodeEntityId(encodedEntityId: string): string;
26
+ /**
27
+ * Segments of a subcollection sitting under the entity `entityId` of `parentPathSegments`.
28
+ *
29
+ * The parent entity id is kept whole however many slashes it contains, while the
30
+ * subcollection's own configured path is spread — a collection path never contains an
31
+ * entity id, so splitting that one is unambiguous.
32
+ *
33
+ * So `["test"] + "test/test" + "accommodation"` yields three segments,
34
+ * `["test", "test/test", "accommodation"]`, where splitting the flattened path
35
+ * `"test/test/test/accommodation"` would wrongly yield four.
36
+ *
37
+ * Returns undefined when the parent segments are unknown: the parent chain cannot be
38
+ * recovered from a flattened path, so there is nothing honest to build on.
39
+ *
40
+ * @group Hooks and utilities
41
+ */
42
+ export declare function buildSubcollectionPathSegments(parentPathSegments: string[] | undefined, entityId: string | undefined, subcollectionPath: string): string[] | undefined;
26
43
  export declare function getLastSegment(path: string): string;
27
- export declare function resolveCollectionPathIds(path: string, allCollections: EntityCollection[]): string;
44
+ /**
45
+ * The outcome of walking a segment chain against a collection tree.
46
+ * @group Hooks and utilities
47
+ */
48
+ export type PathSegmentsWalkStep = {
49
+ collection: EntityCollection;
50
+ /** Resolved path of this collection, e.g. `"products/pid/locales"`. */
51
+ collectionPath: string;
52
+ /** `collectionPath` split at its real segment boundaries. */
53
+ collectionSegments: string[];
54
+ /** The entity addressed inside it, when the chain continues past the collection. */
55
+ entityId?: string;
56
+ };
57
+ export type PathSegmentsWalk = {
58
+ /** Segments with every collection id replaced by the collection's real `path`. */
59
+ resolved: string[];
60
+ /** The collection the chain ends in, if the whole chain matched. */
61
+ collection?: EntityCollection;
62
+ /** One entry per collection level traversed, outermost first. */
63
+ steps: PathSegmentsWalkStep[];
64
+ /** The first segment that could not be matched, if the walk stopped early. */
65
+ unmatched?: string;
66
+ };
67
+ /**
68
+ * Walk a segment chain against a collection tree, matching a collection by either its
69
+ * `path` or its `id` and treating exactly one segment as each entity id.
70
+ *
71
+ * This is the primitive the string-based helpers cannot have: working on the flattened
72
+ * path they must *guess* where an entity id ends by reading up to the next "/", which is
73
+ * wrong for any id that contains one. Here the boundaries are given.
74
+ *
75
+ * Stops at the first segment it cannot match and reports it, rather than throwing.
76
+ *
77
+ * @group Hooks and utilities
78
+ */
79
+ export declare function walkPathSegments(pathSegments: string[], allCollections: EntityCollection[]): PathSegmentsWalk;
80
+ /**
81
+ * Resolve collection aliases in a segment array: every collection id is replaced by the
82
+ * collection's real `path`, and every entity id is carried through untouched.
83
+ *
84
+ * The segment-wise counterpart of {@link resolveCollectionPathIds}, and the reason it
85
+ * exists: that one works on the flattened string, so an entity id containing "/" shifts
86
+ * the rest of the chain by a segment and resolution fails with
87
+ * "Collection definition not found for segment starting with …".
88
+ *
89
+ * Matching accepts either a collection's `path` or its `id`, so already-resolved segments
90
+ * pass through unchanged — the function is idempotent.
91
+ *
92
+ * Mirrors the string version when a chain cannot be matched: it warns and carries the
93
+ * remainder through unresolved, rather than throwing.
94
+ *
95
+ * @group Hooks and utilities
96
+ */
97
+ export declare function resolveCollectionPathSegments(pathSegments: string[], allCollections: EntityCollection[]): string[];
98
+ /**
99
+ * Find the collection a segment chain addresses, whether it ends on the collection itself
100
+ * or on one of its entities.
101
+ *
102
+ * The segment-wise counterpart of {@link getCollectionByPathOrId}, which asserts an odd
103
+ * number of "/"-separated parts — an assertion a slash-bearing entity id fails, throwing
104
+ * where it should simply have resolved.
105
+ *
106
+ * @group Hooks and utilities
107
+ */
108
+ export declare function getCollectionByPathSegments(pathSegments: string[], collections: EntityCollection[]): EntityCollection | undefined;
109
+ export declare function resolveCollectionPathIds(path: string, allCollections: EntityCollection[], pathSegments?: string[]): string;
28
110
  /**
29
111
  * Find the corresponding view at any depth for a given path.
30
112
  * Note that path or segments of the paths can be collection aliases.
31
113
  * @param pathOrId
32
114
  * @param collections
33
115
  */
34
- export declare function getCollectionByPathOrId(pathOrId: string, collections: EntityCollection[]): EntityCollection | undefined;
116
+ export declare function getCollectionByPathOrId(pathOrId: string, collections: EntityCollection[], pathSegments?: string[]): EntityCollection | undefined;
35
117
  /**
36
118
  * Get the subcollection combinations from a path:
37
119
  * "sites/es/locales" => ["sites/es/locales", "sites"]
@@ -1,6 +1,22 @@
1
1
  import { EntityCollection, EntityReference } from "../types";
2
+ /**
3
+ * References to every parent entity in a path.
4
+ *
5
+ * NOTE on encoding: `path` is expected ESCAPED, because it comes from the URL — the entity
6
+ * ids in it are decoded on the way out. Pass `pathSegments` instead whenever the path at
7
+ * hand is a raw datasource path (`entity.path`), which is not escaped and whose ids may
8
+ * therefore contain a bare "/": splitting that would silently produce the wrong references.
9
+ */
2
10
  export declare function getParentReferencesFromPath(props: {
3
11
  path: string;
4
12
  collections: EntityCollection[] | undefined;
5
13
  currentFullPath?: string;
14
+ currentPathSegments?: string[];
15
+ /** `path` split at its real segment boundaries, with RAW ids. Takes precedence. */
16
+ pathSegments?: string[];
6
17
  }): EntityReference[];
18
+ /**
19
+ * The segment-wise counterpart: the boundaries are given, so an entity id keeps its
20
+ * slashes and nothing has to be decoded.
21
+ */
22
+ export declare function getParentReferencesFromPathSegments(pathSegments: string[], collections: EntityCollection[]): EntityReference[];
@@ -9,6 +9,34 @@ export declare function segmentsToStrippedPath(paths: string[]): string;
9
9
  /**
10
10
  * Extract the collection path routes
11
11
  * `products/B44RG6APH/locales` => [`products`, `locales`]
12
+ *
13
+ * Finds the entity ids by position, which only works while no id contains "/" — with one,
14
+ * every following segment shifts and the wrong elements are kept. Use
15
+ * {@link collectionSegmentsFrom} when the real boundaries are known.
16
+ *
12
17
  * @param path
13
18
  */
14
19
  export declare function fullPathToCollectionSegments(path: string): string[];
20
+ /**
21
+ * The collection chain of an already-segmented path:
22
+ * `['products', 'B44RG6APH', 'locales']` => [`products`, `locales`]
23
+ *
24
+ * The counterpart of {@link fullPathToCollectionSegments} for callers that know the real
25
+ * segment boundaries. Kept as a separate function rather than an extra parameter on that
26
+ * one: these helpers are small enough to be passed point-free, and `paths.map(fn)` would
27
+ * then hand the array index in as the new argument.
28
+ */
29
+ export declare function collectionSegmentsFrom(pathSegments: string[]): string[];
30
+ /**
31
+ * The historical, positional reading of a flattened path:
32
+ * `products/B44RG6APH/locales` => [`products`, `locales`]
33
+ *
34
+ * This produces a **collection chain**, not a datasource `pathSegments` value, and must
35
+ * never be used as one — see `path_segments_no_fabrication.test.ts`. It is only correct
36
+ * while no entity id contains "/", which is why every caller prefers real segments when
37
+ * they are available, and why it is named for what it assumes.
38
+ *
39
+ * It is still the right reading for a path that came from a URL, where entity ids arrive
40
+ * escaped and so cannot contain a bare "/".
41
+ */
42
+ export declare function positionalCollectionSegments(path: string): string[];
@@ -1,5 +1,11 @@
1
1
  import { AuthController, Entity, EntityCollection, Permissions, User } from "../types";
2
- export declare function resolvePermissions<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): Permissions | undefined;
3
- export declare function canEditEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
4
- export declare function canCreateEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
5
- export declare function canDeleteEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null): boolean;
2
+ /**
3
+ * `pathSegments` is `path` split at its real segment boundaries. It is forwarded so the
4
+ * collection chain handed to a permissions builder stays correct when an entity id contains
5
+ * "/" — splitting `path` positionally would keep the wrong elements. Optional everywhere:
6
+ * omitted, behaviour is exactly as before.
7
+ */
8
+ export declare function resolvePermissions<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null, pathSegments?: string[]): Permissions | undefined;
9
+ export declare function canEditEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null, pathSegments?: string[]): boolean;
10
+ export declare function canCreateEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null, pathSegments?: string[]): boolean;
11
+ export declare function canDeleteEntity<M extends Record<string, any>, USER extends User>(collection: EntityCollection<M>, authController: AuthController<USER>, path: string, entity: Entity<M> | null, pathSegments?: string[]): boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firecms/core",
3
3
  "type": "module",
4
- "version": "3.4.0-canary.63041d0",
4
+ "version": "3.4.0-canary.cb27f16",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -53,8 +53,8 @@
53
53
  "@dnd-kit/core": "^6.3.1",
54
54
  "@dnd-kit/modifiers": "^9.0.0",
55
55
  "@dnd-kit/sortable": "^10.0.0",
56
- "@firecms/formex": "3.4.0-canary.63041d0",
57
- "@firecms/ui": "3.4.0-canary.63041d0",
56
+ "@firecms/formex": "3.4.0-canary.cb27f16",
57
+ "@firecms/ui": "3.4.0-canary.cb27f16",
58
58
  "@floating-ui/dom": "^1.7.4",
59
59
  "@radix-ui/react-portal": "^1.1.10",
60
60
  "@radix-ui/react-slot": "^1.2.4",
@@ -89,7 +89,7 @@
89
89
  "prosemirror-view": "^1.33.8",
90
90
  "react-dropzone": "^14.3.8",
91
91
  "react-fast-compare": "^3.2.2",
92
- "react-i18next": "^14.1.3",
92
+ "react-i18next": "^17.0.11",
93
93
  "react-image-crop": "^11.0.10",
94
94
  "react-markdown": "^9.1.0",
95
95
  "react-moveable": "^0.56.0",
@@ -96,6 +96,8 @@ export function DeleteEntityDialog<M extends Record<string, any>>({
96
96
  deleteEntityWithCallbacks({
97
97
  dataSource,
98
98
  entity,
99
+ // The entity is the authority on where it lives; stated rather than left implicit.
100
+ pathSegments: entity.pathSegments,
99
101
  collection: resolvedCollection,
100
102
  callbacks,
101
103
  onDeleteSuccess,
@@ -3,7 +3,7 @@ import React, { MouseEvent, useCallback } from "react";
3
3
  import { CollectionSize, Entity, EntityAction, EntityCollection, SelectionController } from "../../types";
4
4
  import { Badge, Checkbox, cls, IconButton, Menu, MenuItem, MoreVertIcon, Skeleton, Tooltip } from "@firecms/ui";
5
5
  import { useFireCMSContext, useLargeLayout } from "../../hooks";
6
- import { getEntityFromCache } from "../../util/entity_cache";
6
+ import { entityCacheKey, getEntityFromCache } from "../../util/entity_cache";
7
7
  import { getLocalChangesBackup } from "../../util";
8
8
  import { getChanges } from "../../form/EntityForm";
9
9
 
@@ -82,7 +82,7 @@ export const EntityCollectionRowActions = function EntityCollectionRowActions({
82
82
  const collapsedActions = actions.filter(a => a.collapsed || a.collapsed === undefined);
83
83
  const uncollapsedActions = actions.filter(a => a.collapsed === false);
84
84
  const enableLocalChangesBackup = collection ? getLocalChangesBackup(collection) : false;
85
- const cachedData = enableLocalChangesBackup ? getEntityFromCache(fullPath + "/" + entity.id) : undefined;
85
+ const cachedData = enableLocalChangesBackup ? getEntityFromCache(entityCacheKey(fullPath, entity.id)) : undefined;
86
86
  const hasDraft = (() => {
87
87
  if (!cachedData || typeof cachedData !== "object" || Object.keys(cachedData).length === 0) return false;
88
88
  const realChanges = getChanges(cachedData as any, (entity?.values ?? {}) as any);
@@ -26,6 +26,8 @@ import { OnCellValueChangeParams } from "../../../common";
26
26
  interface PopupFormFieldProps<M extends Record<string, any>> {
27
27
  customFieldValidator?: CustomFieldValidator;
28
28
  path: string;
29
+ /** `path` split at its real segment boundaries; forwarded to the datasource. */
30
+ pathSegments?: string[];
29
31
  entityId: string;
30
32
  tableKey: string;
31
33
  propertyKey?: Extract<keyof M, string>;
@@ -54,6 +56,7 @@ export function PopupFormFieldLoading<M extends Record<string, any>>({
54
56
  propertyKey,
55
57
  collection: inputCollection,
56
58
  path,
59
+ pathSegments,
57
60
  cellRect,
58
61
  open,
59
62
  onClose,
@@ -66,11 +69,12 @@ export function PopupFormFieldLoading<M extends Record<string, any>>({
66
69
  if (entityId && inputCollection) {
67
70
  dataSource.fetchEntity({
68
71
  path,
72
+ pathSegments,
69
73
  entityId,
70
74
  collection: inputCollection
71
75
  }).then(setEntity);
72
76
  }
73
- }, [entityId, inputCollection, dataSource, path]);
77
+ }, [entityId, inputCollection, dataSource, path, pathSegments]);
74
78
 
75
79
  if (!entity) return null;
76
80
  return <PopupFormFieldInternal {...{