@firecms/core 3.4.0-canary.0ef7442 → 3.4.0-canary.1faa2a9

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 (152) hide show
  1. package/README.md +4 -0
  2. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +3 -2
  3. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  4. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  5. package/dist/components/ErrorBoundary.d.ts +1 -3
  6. package/dist/components/LicenseBanner.d.ts +23 -0
  7. package/dist/components/common/useDataSourceTableController.d.ts +4 -2
  8. package/dist/components/common/useTableSearchHelper.d.ts +3 -1
  9. package/dist/components/index.d.ts +1 -0
  10. package/dist/contexts/LicenseStatusContext.d.ts +16 -0
  11. package/dist/core/EntityEditView.d.ts +2 -0
  12. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  13. package/dist/core/index.d.ts +1 -0
  14. package/dist/core/plugin_translations.d.ts +38 -0
  15. package/dist/core/pro_plugins.d.ts +40 -0
  16. package/dist/de-KtBROUvS.js +700 -0
  17. package/dist/de-KtBROUvS.js.map +1 -0
  18. package/dist/editor/extensions/Image.d.ts +11 -0
  19. package/dist/editor/index.d.ts +2 -1
  20. package/dist/editor/lazy_editor.d.ts +6 -0
  21. package/dist/editor-BSo_aS1I.js +4563 -0
  22. package/dist/editor-BSo_aS1I.js.map +1 -0
  23. package/dist/form/EntityForm.d.ts +2 -0
  24. package/dist/form/EntityFormActions.d.ts +2 -0
  25. package/dist/fr-BRFSBaVh.js +700 -0
  26. package/dist/fr-BRFSBaVh.js.map +1 -0
  27. package/dist/hi-DwCkga5X.js +700 -0
  28. package/dist/hi-DwCkga5X.js.map +1 -0
  29. package/dist/hooks/data/delete.d.ts +1 -1
  30. package/dist/hooks/data/useCollectionFetch.d.ts +1 -1
  31. package/dist/hooks/data/useEntityFetch.d.ts +4 -1
  32. package/dist/hooks/index.d.ts +2 -0
  33. package/dist/hooks/useLicenseStatus.d.ts +16 -0
  34. package/dist/hooks/useProjectLog.d.ts +11 -6
  35. package/dist/hooks/useTranslation.d.ts +1 -1
  36. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  37. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  38. package/dist/index.es.js +3503 -11244
  39. package/dist/index.es.js.map +1 -1
  40. package/dist/index.umd.js +8028 -7006
  41. package/dist/index.umd.js.map +1 -1
  42. package/dist/it-B2YGgDsx.js +700 -0
  43. package/dist/it-B2YGgDsx.js.map +1 -0
  44. package/dist/pl-BxHkywK8.js +702 -0
  45. package/dist/pl-BxHkywK8.js.map +1 -0
  46. package/dist/pt-q6UV4diT.js +702 -0
  47. package/dist/pt-q6UV4diT.js.map +1 -0
  48. package/dist/types/analytics.d.ts +1 -1
  49. package/dist/types/collections.d.ts +7 -0
  50. package/dist/types/datasource.d.ts +8 -0
  51. package/dist/types/entity_actions.d.ts +6 -0
  52. package/dist/types/entity_callbacks.d.ts +21 -0
  53. package/dist/types/firecms.d.ts +18 -1
  54. package/dist/types/firecms_context.d.ts +8 -0
  55. package/dist/types/index.d.ts +1 -0
  56. package/dist/types/license.d.ts +54 -0
  57. package/dist/types/navigation.d.ts +34 -5
  58. package/dist/types/plugins.d.ts +22 -1
  59. package/dist/types/side_entity_controller.d.ts +3 -2
  60. package/dist/types/translations.d.ts +31 -2
  61. package/dist/util/date_fns_locales.d.ts +18 -0
  62. package/dist/util/entity_cache.d.ts +11 -0
  63. package/dist/util/index.d.ts +1 -0
  64. package/dist/util/lazy_eager.d.ts +3 -3
  65. package/dist/util/navigation_utils.d.ts +84 -2
  66. package/dist/util/parent_references_from_path.d.ts +15 -0
  67. package/dist/util/paths.d.ts +28 -0
  68. package/dist/util/permissions.d.ts +10 -4
  69. package/package.json +3 -3
  70. package/src/app/Scaffold.tsx +7 -0
  71. package/src/components/DeleteEntityDialog.tsx +2 -0
  72. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  73. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +8 -0
  74. package/src/components/EntityCollectionView/EntityCollectionView.tsx +40 -15
  75. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  76. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
  77. package/src/components/EntityCollectionView/useBoardDataController.tsx +7 -2
  78. package/src/components/EntityPreview.tsx +4 -1
  79. package/src/components/ErrorBoundary.tsx +12 -18
  80. package/src/components/LicenseBanner.tsx +209 -0
  81. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +8 -2
  82. package/src/components/common/default_entity_actions.tsx +22 -2
  83. package/src/components/common/useDataSourceTableController.tsx +26 -8
  84. package/src/components/common/useTableSearchHelper.ts +5 -0
  85. package/src/components/index.tsx +1 -0
  86. package/src/contexts/LicenseStatusContext.tsx +50 -0
  87. package/src/core/DefaultAppBar.tsx +28 -16
  88. package/src/core/EntityEditView.tsx +8 -12
  89. package/src/core/EntityEditViewFormActions.tsx +3 -2
  90. package/src/core/EntitySidePanel.tsx +6 -4
  91. package/src/core/FireCMS.tsx +73 -47
  92. package/src/core/SideDialogs.tsx +44 -1
  93. package/src/core/index.tsx +1 -0
  94. package/src/core/plugin_translations.ts +87 -0
  95. package/src/core/pro_plugins.ts +79 -0
  96. package/src/editor/components/SlashCommandMenu.tsx +4 -1
  97. package/src/editor/extensions/Image.ts +17 -1
  98. package/src/editor/hooks/useProseMirror.ts +5 -0
  99. package/src/editor/index.ts +11 -1
  100. package/src/editor/lazy_editor.tsx +33 -0
  101. package/src/form/EntityForm.tsx +22 -7
  102. package/src/form/EntityFormActions.tsx +2 -0
  103. package/src/hooks/data/delete.ts +8 -2
  104. package/src/hooks/data/save.ts +4 -1
  105. package/src/hooks/data/useCollectionFetch.tsx +9 -2
  106. package/src/hooks/data/useEntityFetch.tsx +19 -6
  107. package/src/hooks/index.tsx +2 -0
  108. package/src/hooks/useBuildNavigationController.tsx +70 -15
  109. package/src/hooks/useFireCMSContext.tsx +7 -3
  110. package/src/hooks/useLicenseStatus.tsx +32 -0
  111. package/src/hooks/useProjectLog.tsx +27 -9
  112. package/src/hooks/useResolvedNavigationFrom.tsx +1 -1
  113. package/src/hooks/useTranslation.ts +3 -1
  114. package/src/i18n/FireCMSi18nProvider.tsx +199 -25
  115. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  116. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  117. package/src/internal/useBuildDataSource.ts +13 -12
  118. package/src/internal/useBuildSideEntityController.tsx +13 -5
  119. package/src/locales/de.ts +16 -2
  120. package/src/locales/en.ts +18 -2
  121. package/src/locales/es.ts +16 -2
  122. package/src/locales/fr.ts +16 -2
  123. package/src/locales/hi.ts +16 -2
  124. package/src/locales/it.ts +16 -2
  125. package/src/locales/pl.ts +16 -2
  126. package/src/locales/pt.ts +16 -2
  127. package/src/preview/components/DatePreview.tsx +2 -4
  128. package/src/preview/components/ReferencePreview.tsx +7 -3
  129. package/src/routes/FireCMSRoute.tsx +9 -4
  130. package/src/types/analytics.ts +3 -0
  131. package/src/types/collections.ts +8 -0
  132. package/src/types/datasource.ts +8 -0
  133. package/src/types/entity_actions.tsx +6 -0
  134. package/src/types/entity_callbacks.ts +24 -0
  135. package/src/types/firecms.tsx +19 -1
  136. package/src/types/firecms_context.tsx +9 -0
  137. package/src/types/index.ts +1 -0
  138. package/src/types/license.ts +62 -0
  139. package/src/types/navigation.ts +35 -5
  140. package/src/types/plugins.tsx +23 -1
  141. package/src/types/side_entity_controller.tsx +3 -2
  142. package/src/types/translations.ts +31 -2
  143. package/src/util/date_fns_locales.ts +168 -0
  144. package/src/util/entity_cache.ts +18 -0
  145. package/src/util/index.ts +1 -0
  146. package/src/util/lazy_eager.tsx +30 -8
  147. package/src/util/navigation_from_path.ts +6 -1
  148. package/src/util/navigation_utils.ts +204 -2
  149. package/src/util/parent_references_from_path.ts +32 -1
  150. package/src/util/paths.ts +40 -3
  151. package/src/util/permissions.ts +22 -10
  152. package/src/util/useStorageUploadController.tsx +6 -1
@@ -0,0 +1,18 @@
1
+ import type { Locale as DateFnsLocale } from "date-fns";
2
+ import { Locale } from "../types";
3
+ /**
4
+ * Load a date-fns locale, resolving from cache when it is already in memory.
5
+ * Concurrent callers for the same locale share one request.
6
+ */
7
+ export declare function loadDateFnsLocale(locale: Locale): Promise<DateFnsLocale | undefined>;
8
+ /** The locale if it is already loaded, otherwise undefined. Never suspends. */
9
+ export declare function getLoadedDateFnsLocale(locale?: Locale): DateFnsLocale | undefined;
10
+ /**
11
+ * The date-fns locale for `locale`, loading it in the background on first use.
12
+ *
13
+ * Returns undefined until the chunk arrives, which formats the first paint with
14
+ * date-fns' built-in English and then re-renders. That is a change only for
15
+ * projects that set a locale, and only for the few hundred milliseconds before
16
+ * the chunk lands — in exchange for every project no longer waiting on 625KB.
17
+ */
18
+ export declare function useDateFnsLocale(locale?: Locale): DateFnsLocale | undefined;
@@ -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.
@@ -28,3 +28,4 @@ export * from "./storage";
28
28
  export * from "./callbacks";
29
29
  export * from "./conditions";
30
30
  export * from "./lazy_eager";
31
+ export * from "./date_fns_locales";
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  /**
3
- * Returns a React.lazy component that is also preloaded immediately using
4
- * requestIdleCallback or setTimeout.
5
- * This ensures that chunks are split, but fetched in the background before they are actually needed.
3
+ * Returns a React.lazy component that is also preloaded in the background, once
4
+ * the page has finished loading.
5
+ * This ensures that chunks are split, but fetched before they are actually needed.
6
6
  */
7
7
  export declare function lazyEager<T extends React.ComponentType<any>>(factory: () => Promise<any>, exportName?: string): React.LazyExoticComponent<T>;
@@ -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,7 +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;
6
14
  currentPathSegments?: string[];
15
+ /** `path` split at its real segment boundaries, with RAW ids. Takes precedence. */
16
+ pathSegments?: string[];
7
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.0ef7442",
4
+ "version": "3.4.0-canary.1faa2a9",
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.0ef7442",
57
- "@firecms/ui": "3.4.0-canary.0ef7442",
56
+ "@firecms/formex": "3.4.0-canary.1faa2a9",
57
+ "@firecms/ui": "3.4.0-canary.1faa2a9",
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",
@@ -3,6 +3,7 @@ import { cls, defaultBorderMixin, IconButton, MenuIcon, Sheet } from "@firecms/u
3
3
  import equal from "react-fast-compare"
4
4
  import { useLargeLayout } from "../hooks";
5
5
  import { ErrorBoundary } from "../components";
6
+ import { LicenseBanner } from "../components/LicenseBanner";
6
7
  import { AppContext } from "./useApp";
7
8
  import { useTranslation } from "../hooks/useTranslation";
8
9
 
@@ -146,6 +147,12 @@ export const Scaffold = React.memo<PropsWithChildren<ScaffoldProps>>(
146
147
 
147
148
  {hasAppBar && <DrawerHeader />}
148
149
 
150
+ <LicenseBanner
151
+ className={cls("flex-shrink-0", {
152
+ "px-2 pb-1 lg:px-4 lg:pb-2": padding,
153
+ "pt-2 lg:pt-4": !hasAppBar
154
+ })}/>
155
+
149
156
  <div
150
157
  className={cls(defaultBorderMixin, "flex-grow overflow-auto m-0 ", {
151
158
  "lg:mt-4": !hasAppBar,
@@ -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);
@@ -274,6 +274,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
274
274
 
275
275
  currentDataSource.countEntities({
276
276
  path: fullPath,
277
+ pathSegments,
277
278
  collection: currentCollection
278
279
  }).then(count => {
279
280
  totalCount = count;
@@ -285,6 +286,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
285
286
 
286
287
  currentDataSource.countEntities({
287
288
  path: fullPath,
289
+ pathSegments,
288
290
  collection: currentCollection,
289
291
  filter: { [orderProperty]: ["!=", null] } as FilterValues<string>
290
292
  }).then(count => {
@@ -436,6 +438,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
436
438
 
437
439
  const saveProps: SaveEntityProps = {
438
440
  path: entity.path,
441
+ // Paired with `entity.path`: the segments the entity was loaded with.
442
+ pathSegments: entity.pathSegments,
439
443
  entityId: entity.id,
440
444
  values: updatedValues as M,
441
445
  previousValues: entity.values,
@@ -476,6 +480,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
476
480
  console.log("Fetching all documents from collection...");
477
481
  const allDocs = await dataSource.fetchCollection<M>({
478
482
  path: fullPath,
483
+ pathSegments,
479
484
  collection,
480
485
  limit: 10000 // Fetch all
481
486
  });
@@ -496,6 +501,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
496
501
 
497
502
  const saveProps: SaveEntityProps = {
498
503
  path: entity.path,
504
+ // Paired with `entity.path`: the segments the entity was loaded with.
505
+ pathSegments: entity.pathSegments,
499
506
  entityId: entity.id,
500
507
  values: updatedValues as M,
501
508
  previousValues: entity.values,
@@ -667,6 +674,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
667
674
  });
668
675
  sideEntityController.open({
669
676
  path: fullPath,
677
+ pathSegments,
670
678
  collection,
671
679
  entityId: undefined,
672
680
  updateUrl: true,
@@ -90,6 +90,7 @@ import { useSelectionController } from "./useSelectionController";
90
90
  import { EntityCollectionViewStartActions } from "./EntityCollectionViewStartActions";
91
91
  import { addRecentId, getRecentIds } from "./utils";
92
92
  import { useScrollRestoration } from "../common/useScrollRestoration";
93
+ import { entityCacheKey } from "../../util/entity_cache";
93
94
 
94
95
  const DEFAULT_ENTITY_OPEN_MODE: "side_panel" | "full_screen" = "side_panel";
95
96
 
@@ -109,8 +110,9 @@ export type EntityCollectionViewProps<M extends Record<string, any>> = {
109
110
  /**
110
111
  * `fullPath` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
111
112
  * Forwarded to the datasource so a parent entity id containing "/" stays unambiguous.
112
- * Defaults to splitting `fullPath`, which is correct for any backend whose ids cannot
113
- * contain "/".
113
+ *
114
+ * Optional, and never derived by splitting `fullPath` — a guess would be wrong in
115
+ * exactly the case the field exists for. Absent means "not known here", not "no slashes".
114
116
  */
115
117
  pathSegments?: string[];
116
118
  /**
@@ -198,7 +200,7 @@ export const EntityCollectionView = React.memo(
198
200
  collectionRef.current = collection;
199
201
  }, [collection]);
200
202
 
201
- const canCreateEntities = canCreateEntity(collection, authController, fullPath, null);
203
+ const canCreateEntities = canCreateEntity(collection, authController, fullPath, null, pathSegments);
202
204
  const [highlightedEntity, setHighlightedEntity] = useState<Entity<M> | undefined>(undefined);
203
205
  const [deleteEntityClicked, setDeleteEntityClicked] = React.useState<Entity<M> | Entity<M>[] | undefined>(undefined);
204
206
 
@@ -228,7 +230,7 @@ export const EntityCollectionView = React.memo(
228
230
 
229
231
  const checkInlineEditing = useCallback((entity?: Entity<any>): boolean => {
230
232
  const collection = collectionRef.current;
231
- if (!canEditEntity(collection, authController, fullPath, entity ?? null)) {
233
+ if (!canEditEntity(collection, authController, fullPath, entity ?? null, pathSegments)) {
232
234
  return false;
233
235
  }
234
236
  return collection.inlineEditing === undefined || collection.inlineEditing;
@@ -351,10 +353,16 @@ export const EntityCollectionView = React.memo(
351
353
  addRecentId(collection.id, clickedEntity.id);
352
354
  }
353
355
 
354
- const path = collection?.collectionGroup ? clickedEntity.path : (fullPath ?? clickedEntity.path);
356
+ // In a collection group the row's own path is the real one, so the row's segments
357
+ // are the matching ones; otherwise both come from this view. `path` and
358
+ // `pathSegments` are picked from the same source so they cannot describe
359
+ // different chains.
360
+ const isCollectionGroup = Boolean(collection?.collectionGroup);
361
+ const path = isCollectionGroup ? clickedEntity.path : (fullPath ?? clickedEntity.path);
355
362
  navigateToEntity({
356
363
  navigation,
357
364
  path,
365
+ pathSegments: isCollectionGroup ? clickedEntity.pathSegments : (pathSegments ?? clickedEntity.pathSegments),
358
366
  fullIdPath,
359
367
  sideEntityController,
360
368
  openEntityMode,
@@ -461,7 +469,7 @@ export const EntityCollectionView = React.memo(
461
469
  }
462
470
  }, [setViewMode, userConfigPersistence, onCollectionModifiedForUser, fullPath, analyticsController, viewMode]);
463
471
 
464
- const createEnabled = canCreateEntity(collection, authController, fullPath, null);
472
+ const createEnabled = canCreateEntity(collection, authController, fullPath, null, pathSegments);
465
473
 
466
474
  const uniqueFieldValidator: UniqueFieldValidator = useCallback(
467
475
  ({
@@ -469,8 +477,8 @@ export const EntityCollectionView = React.memo(
469
477
  value,
470
478
  property,
471
479
  entityId
472
- }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection),
473
- [fullPath]);
480
+ }) => dataSource.checkUniqueField(fullPath, name, value, entityId, collection, pathSegments),
481
+ [fullPath, pathSegments]);
474
482
 
475
483
  const onValueChange: OnCellValueChange<any, any> = ({
476
484
  value,
@@ -484,6 +492,8 @@ export const EntityCollectionView = React.memo(
484
492
 
485
493
  const saveProps: SaveEntityProps = {
486
494
  path: entity.path ?? fullPath,
495
+ // Paired with the `path` above: the row's segments describe the row's path.
496
+ pathSegments: entity.path ? entity.pathSegments : pathSegments,
487
497
  entityId: entity.id,
488
498
  values: updatedValues,
489
499
  previousValues: entity.values,
@@ -514,7 +524,7 @@ export const EntityCollectionView = React.memo(
514
524
 
515
525
  };
516
526
 
517
- const resolvedFullPath = navigation.resolveIdsFrom(fullPath);
527
+ const resolvedFullPath = navigation.resolveIdsFrom(fullPath, pathSegments);
518
528
  const resolvedCollection = useMemo(() => resolveCollection<M>({
519
529
  collection,
520
530
  path: fullPath,
@@ -676,13 +686,15 @@ export const EntityCollectionView = React.memo(
676
686
  width: 260,
677
687
  dependencies: [],
678
688
  Builder: ({ entity }) => {
679
- const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path);
689
+ // `entity.path` is a RAW datasource path, so its segments are passed:
690
+ // splitting it would mis-read any parent id containing "/".
691
+ const collectionsWithPath = navigation.getParentReferencesFromPath(entity.path, entity.pathSegments);
680
692
  return (
681
693
  <div className={"flex flex-col gap-2 w-full"}>
682
694
  {collectionsWithPath.map((reference) => {
683
695
  return (
684
696
  <ReferencePreview
685
- key={reference.path + "/" + reference.id}
697
+ key={entityCacheKey(reference.path, reference.id)}
686
698
  reference={reference}
687
699
  size={"small"} />
688
700
  );
@@ -713,7 +725,7 @@ export const EntityCollectionView = React.memo(
713
725
  entity?: Entity<M>,
714
726
  customEntityActions?: EntityAction[]
715
727
  }): EntityAction[] => {
716
- const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity) : true;
728
+ const deleteEnabled = entity ? canDeleteEntity(collection, authController, fullPath, entity, entity.pathSegments ?? pathSegments) : true;
717
729
  const actions: EntityAction[] = [
718
730
  { ...editEntityAction, name: t("edit") }
719
731
  ];
@@ -843,6 +855,7 @@ export const EntityCollectionView = React.memo(
843
855
  onTextSearchClick,
844
856
  textSearchEnabled
845
857
  } = useTableSearchHelper({
858
+ pathSegments,
846
859
  collection,
847
860
  fullPath: resolvedFullPath,
848
861
  parentCollectionIds,
@@ -907,6 +920,7 @@ export const EntityCollectionView = React.memo(
907
920
  collection={collection}
908
921
  tableController={tableController}
909
922
  path={fullPath}
923
+ pathSegments={pathSegments}
910
924
  relativePath={collection.path}
911
925
  selectionController={usedSelectionController}
912
926
  collectionEntitiesCount={docsCount}
@@ -918,6 +932,7 @@ export const EntityCollectionView = React.memo(
918
932
  onMultipleDeleteClick={onMultipleDeleteClick}
919
933
  onNewClick={onNewClick}
920
934
  path={fullPath}
935
+ pathSegments={pathSegments}
921
936
  relativePath={collection.path}
922
937
  selectionController={usedSelectionController}
923
938
  selectionEnabled={selectionEnabled}
@@ -1026,6 +1041,7 @@ export const EntityCollectionView = React.memo(
1026
1041
  getIdColumnWidth={getIdColumnWidth}
1027
1042
  additionalIDHeaderWidget={<EntityIdHeaderWidget
1028
1043
  path={fullPath}
1044
+ pathSegments={pathSegments}
1029
1045
  fullIdPath={fullIdPath ?? fullPath}
1030
1046
  collection={collection} />}
1031
1047
  openEntityMode={openEntityMode}
@@ -1140,8 +1156,12 @@ function EntitiesCount({
1140
1156
 
1141
1157
  const sortByProperty = sortBy ? sortBy[0] : undefined;
1142
1158
  const currentSort = sortBy ? sortBy[1] : undefined;
1143
- const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
1144
- const resolvedPathSegments = pathSegments;
1159
+ // Resolved together so the two representations always describe the same chain.
1160
+ // Segments are resolved, never derived (see useEntityFetch).
1161
+ const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
1162
+ const resolvedPathSegments = useMemo(() => pathSegments
1163
+ ? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
1164
+ : undefined, [pathSegments, navigation.resolveSegmentsFrom]);
1145
1165
 
1146
1166
  useEffect(() => {
1147
1167
  if (dataSource.countEntities)
@@ -1182,10 +1202,13 @@ function buildPropertyWidthOverwrite(key: string, width: number): PartialEntityC
1182
1202
  function EntityIdHeaderWidget({
1183
1203
  collection,
1184
1204
  path,
1205
+ pathSegments,
1185
1206
  fullIdPath
1186
1207
  }: {
1187
1208
  collection: EntityCollection,
1188
1209
  path: string,
1210
+ /** `path` split at its real segment boundaries, when the caller knows them. */
1211
+ pathSegments?: string[],
1189
1212
  fullIdPath: string
1190
1213
  }) {
1191
1214
 
@@ -1225,6 +1248,7 @@ function EntityIdHeaderWidget({
1225
1248
  collection,
1226
1249
  entityId,
1227
1250
  path,
1251
+ pathSegments,
1228
1252
  fullIdPath,
1229
1253
  sideEntityController,
1230
1254
  navigation
@@ -1250,7 +1274,7 @@ function EntityIdHeaderWidget({
1250
1274
  </form>
1251
1275
  {recentIds && recentIds.length > 0 && <div className="flex flex-col gap-2 p-2">
1252
1276
  {recentIds.map(id => (
1253
- <ReferencePreview reference={new EntityReference(id, path)}
1277
+ <ReferencePreview reference={new EntityReference(id, path, undefined, pathSegments)}
1254
1278
  key={id}
1255
1279
  hover={true}
1256
1280
  onClick={() => {
@@ -1260,6 +1284,7 @@ function EntityIdHeaderWidget({
1260
1284
  collection,
1261
1285
  entityId: id,
1262
1286
  path,
1287
+ pathSegments,
1263
1288
  fullIdPath,
1264
1289
  sideEntityController,
1265
1290
  navigation
@@ -22,6 +22,8 @@ import { useTranslation } from "../../hooks/useTranslation";
22
22
  export type EntityCollectionViewActionsProps<M extends Record<string, any>> = {
23
23
  collection: EntityCollection<M>;
24
24
  path: string;
25
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
26
+ pathSegments?: string[];
25
27
  relativePath: string;
26
28
  parentCollectionIds: string[];
27
29
  selectionEnabled: boolean;
@@ -40,6 +42,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
40
42
  onMultipleDeleteClick,
41
43
  selectionEnabled,
42
44
  path,
45
+ pathSegments,
43
46
  selectionController,
44
47
  tableController,
45
48
  collectionEntitiesCount,
@@ -56,7 +59,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
56
59
 
57
60
  const selectedEntities = selectionController.selectedEntities;
58
61
 
59
- const addButton = canCreateEntity(collection, authController, path, null) &&
62
+ const addButton = canCreateEntity(collection, authController, path, null, pathSegments) &&
60
63
  onNewClick && (largeLayout
61
64
  ? <Button
62
65
  id={`add_entity_${path}`}
@@ -75,7 +78,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
75
78
  <AddIcon size={"small"} />
76
79
  </Button>);
77
80
 
78
- const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null);
81
+ const multipleDeleteEnabled = canDeleteEntity(collection, authController, path, null, pathSegments);
79
82
 
80
83
  let multipleDeleteButton: React.ReactNode | undefined;
81
84
  if (selectionEnabled) {
@@ -107,6 +110,7 @@ export function EntityCollectionViewActions<M extends Record<string, any>>({
107
110
 
108
111
  const actionProps: CollectionActionsProps = {
109
112
  path,
113
+ pathSegments,
110
114
  relativePath,
111
115
  parentCollectionIds,
112
116
  collection,
@@ -17,6 +17,8 @@ import { useTranslation } from "../../hooks/useTranslation";
17
17
  export type EntityCollectionViewStartActionsProps<M extends Record<string, any>> = {
18
18
  collection: EntityCollection<M>;
19
19
  path: string;
20
+ /** `path` split at its real segment boundaries, when known. Never derived from `path`. */
21
+ pathSegments?: string[];
20
22
  relativePath: string;
21
23
  parentCollectionIds: string[];
22
24
  selectionController: SelectionController<M>;
@@ -33,6 +35,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
33
35
  relativePath,
34
36
  parentCollectionIds,
35
37
  path,
38
+ pathSegments,
36
39
  selectionController,
37
40
  tableController,
38
41
  collectionEntitiesCount,
@@ -57,6 +60,7 @@ export function EntityCollectionViewStartActions<M extends Record<string, any>>(
57
60
 
58
61
  const actionProps: CollectionActionsProps = {
59
62
  path,
63
+ pathSegments,
60
64
  relativePath,
61
65
  parentCollectionIds,
62
66
  collection,
@@ -82,8 +82,12 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
82
82
  const context = useFireCMSContext();
83
83
  const dataSource = useDataSource(collection);
84
84
  const navigation = useNavigationController();
85
- const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath), [fullPath, navigation.resolveIdsFrom]);
86
- const resolvedPathSegments = pathSegments;
85
+ // Resolved together so the two representations always describe the same chain.
86
+ // Segments are resolved, never derived (see useEntityFetch).
87
+ const resolvedPath = useMemo(() => navigation.resolveIdsFrom(fullPath, pathSegments), [fullPath, pathSegments, navigation.resolveIdsFrom]);
88
+ const resolvedPathSegments = useMemo(() => pathSegments
89
+ ? (navigation.resolveSegmentsFrom?.(pathSegments) ?? pathSegments)
90
+ : undefined, [pathSegments, navigation.resolveSegmentsFrom]);
87
91
 
88
92
  // Stable refs for objects that shouldn't trigger re-subscriptions
89
93
  const dataSourceRef = useRef(dataSource);
@@ -212,6 +216,7 @@ export function useBoardDataController<M extends Record<string, any> = any, COLU
212
216
  currentCollection.callbacks!.onFetch!({
213
217
  collection: currentCollection,
214
218
  path: currentResolvedPath,
219
+ pathSegments: currentPathSegments,
215
220
  entity,
216
221
  context: currentContext
217
222
  })