@firecms/core 3.4.0-canary.63041d0 → 3.4.0-canary.9408c41

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 (160) hide show
  1. package/README.md +4 -0
  2. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  3. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  4. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +3 -2
  5. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  7. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  8. package/dist/components/ErrorBoundary.d.ts +1 -3
  9. package/dist/components/LicenseBanner.d.ts +23 -0
  10. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  11. package/dist/components/common/useDataSourceTableController.d.ts +4 -2
  12. package/dist/components/common/useTableSearchHelper.d.ts +3 -1
  13. package/dist/components/index.d.ts +1 -0
  14. package/dist/contexts/LicenseStatusContext.d.ts +16 -0
  15. package/dist/core/EntityEditView.d.ts +2 -0
  16. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  17. package/dist/core/index.d.ts +1 -0
  18. package/dist/core/plugin_translations.d.ts +38 -0
  19. package/dist/core/pro_plugins.d.ts +40 -0
  20. package/dist/de-DMp6cIY1.js +702 -0
  21. package/dist/de-DMp6cIY1.js.map +1 -0
  22. package/dist/editor/extensions/Image.d.ts +11 -0
  23. package/dist/editor/index.d.ts +2 -1
  24. package/dist/editor/lazy_editor.d.ts +6 -0
  25. package/dist/editor-BSo_aS1I.js +4563 -0
  26. package/dist/editor-BSo_aS1I.js.map +1 -0
  27. package/dist/form/EntityForm.d.ts +8 -1
  28. package/dist/form/EntityFormActions.d.ts +2 -0
  29. package/dist/fr-Cckz4IkR.js +702 -0
  30. package/dist/fr-Cckz4IkR.js.map +1 -0
  31. package/dist/hi-CDCoIqbu.js +702 -0
  32. package/dist/hi-CDCoIqbu.js.map +1 -0
  33. package/dist/hooks/data/delete.d.ts +1 -1
  34. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  35. package/dist/hooks/data/useEntityFetch.d.ts +4 -1
  36. package/dist/hooks/index.d.ts +2 -0
  37. package/dist/hooks/useLicenseStatus.d.ts +16 -0
  38. package/dist/hooks/useProjectLog.d.ts +11 -6
  39. package/dist/hooks/useTranslation.d.ts +1 -1
  40. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  41. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  42. package/dist/index.es.js +3113 -10785
  43. package/dist/index.es.js.map +1 -1
  44. package/dist/index.umd.js +7939 -6836
  45. package/dist/index.umd.js.map +1 -1
  46. package/dist/it-BJCUqixj.js +702 -0
  47. package/dist/it-BJCUqixj.js.map +1 -0
  48. package/dist/pl-DgT_joFf.js +704 -0
  49. package/dist/pl-DgT_joFf.js.map +1 -0
  50. package/dist/pt-B3dN3x0X.js +704 -0
  51. package/dist/pt-B3dN3x0X.js.map +1 -0
  52. package/dist/types/analytics.d.ts +1 -1
  53. package/dist/types/collections.d.ts +7 -0
  54. package/dist/types/datasource.d.ts +25 -4
  55. package/dist/types/entities.d.ts +21 -1
  56. package/dist/types/entity_actions.d.ts +6 -0
  57. package/dist/types/entity_callbacks.d.ts +21 -0
  58. package/dist/types/firecms.d.ts +18 -1
  59. package/dist/types/firecms_context.d.ts +8 -0
  60. package/dist/types/index.d.ts +1 -0
  61. package/dist/types/license.d.ts +54 -0
  62. package/dist/types/navigation.d.ts +34 -5
  63. package/dist/types/plugins.d.ts +22 -1
  64. package/dist/types/side_entity_controller.d.ts +3 -2
  65. package/dist/types/translations.d.ts +35 -2
  66. package/dist/util/date_fns_locales.d.ts +18 -0
  67. package/dist/util/entity_cache.d.ts +11 -0
  68. package/dist/util/index.d.ts +1 -0
  69. package/dist/util/lazy_eager.d.ts +3 -3
  70. package/dist/util/navigation_utils.d.ts +84 -2
  71. package/dist/util/parent_references_from_path.d.ts +16 -0
  72. package/dist/util/paths.d.ts +28 -0
  73. package/dist/util/permissions.d.ts +10 -4
  74. package/package.json +5 -6
  75. package/src/app/Scaffold.tsx +7 -0
  76. package/src/components/DeleteEntityDialog.tsx +2 -0
  77. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  78. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  79. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  80. package/src/components/EntityCollectionView/EntityCollectionView.tsx +49 -16
  81. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  82. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
  83. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  84. package/src/components/EntityPreview.tsx +4 -1
  85. package/src/components/ErrorBoundary.tsx +12 -18
  86. package/src/components/LicenseBanner.tsx +209 -0
  87. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  88. package/src/components/common/default_entity_actions.tsx +22 -2
  89. package/src/components/common/useDataSourceTableController.tsx +32 -8
  90. package/src/components/common/useTableSearchHelper.ts +5 -0
  91. package/src/components/index.tsx +1 -0
  92. package/src/contexts/LicenseStatusContext.tsx +50 -0
  93. package/src/core/DefaultAppBar.tsx +28 -16
  94. package/src/core/EntityEditView.tsx +11 -12
  95. package/src/core/EntityEditViewFormActions.tsx +3 -2
  96. package/src/core/EntitySidePanel.tsx +6 -4
  97. package/src/core/FireCMS.tsx +73 -47
  98. package/src/core/SideDialogs.tsx +44 -1
  99. package/src/core/index.tsx +1 -0
  100. package/src/core/plugin_translations.ts +87 -0
  101. package/src/core/pro_plugins.ts +79 -0
  102. package/src/editor/components/SlashCommandMenu.tsx +4 -1
  103. package/src/editor/extensions/Image.ts +17 -1
  104. package/src/editor/hooks/useProseMirror.ts +5 -0
  105. package/src/editor/index.ts +11 -1
  106. package/src/editor/lazy_editor.tsx +33 -0
  107. package/src/form/EntityForm.tsx +30 -9
  108. package/src/form/EntityFormActions.tsx +2 -0
  109. package/src/hooks/data/delete.ts +8 -0
  110. package/src/hooks/data/save.ts +5 -2
  111. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  112. package/src/hooks/data/useEntityFetch.tsx +28 -6
  113. package/src/hooks/index.tsx +2 -0
  114. package/src/hooks/useBuildNavigationController.tsx +70 -15
  115. package/src/hooks/useFireCMSContext.tsx +7 -3
  116. package/src/hooks/useLicenseStatus.tsx +32 -0
  117. package/src/hooks/useProjectLog.tsx +27 -9
  118. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  119. package/src/hooks/useTranslation.ts +3 -1
  120. package/src/i18n/FireCMSi18nProvider.tsx +199 -25
  121. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  122. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  123. package/src/internal/useBuildDataSource.ts +26 -15
  124. package/src/internal/useBuildSideEntityController.tsx +13 -5
  125. package/src/locales/de.ts +18 -2
  126. package/src/locales/en.ts +21 -3
  127. package/src/locales/es.ts +18 -2
  128. package/src/locales/fr.ts +18 -2
  129. package/src/locales/hi.ts +18 -2
  130. package/src/locales/it.ts +18 -2
  131. package/src/locales/pl.ts +18 -2
  132. package/src/locales/pt.ts +18 -2
  133. package/src/preview/components/DatePreview.tsx +2 -4
  134. package/src/preview/components/ReferencePreview.tsx +7 -3
  135. package/src/routes/FireCMSRoute.tsx +9 -4
  136. package/src/types/analytics.ts +3 -0
  137. package/src/types/collections.ts +8 -0
  138. package/src/types/datasource.ts +25 -4
  139. package/src/types/entities.ts +24 -1
  140. package/src/types/entity_actions.tsx +6 -0
  141. package/src/types/entity_callbacks.ts +24 -0
  142. package/src/types/firecms.tsx +19 -1
  143. package/src/types/firecms_context.tsx +9 -0
  144. package/src/types/index.ts +1 -0
  145. package/src/types/license.ts +62 -0
  146. package/src/types/navigation.ts +35 -5
  147. package/src/types/plugins.tsx +23 -1
  148. package/src/types/side_entity_controller.tsx +3 -2
  149. package/src/types/translations.ts +35 -2
  150. package/src/util/date_fns_locales.ts +168 -0
  151. package/src/util/entities.ts +3 -1
  152. package/src/util/entity_cache.ts +18 -0
  153. package/src/util/index.ts +1 -0
  154. package/src/util/lazy_eager.tsx +30 -8
  155. package/src/util/navigation_from_path.ts +6 -1
  156. package/src/util/navigation_utils.ts +210 -3
  157. package/src/util/parent_references_from_path.ts +42 -3
  158. package/src/util/paths.ts +40 -3
  159. package/src/util/permissions.ts +22 -10
  160. package/src/util/useStorageUploadController.tsx +6 -1
@@ -13,6 +13,8 @@ import { InternalUserManagement } from "./internal_user_management";
13
13
  * NOTE: This is a work in progress and the API is not stable yet.
14
14
  * @group Core
15
15
  */
16
+ /** The strings a plugin contributes for one locale. */
17
+ export type PluginTranslations = Record<string, string>;
16
18
  export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollection = EntityCollection, COL_ACTIONS_PROPS = any, COL_ACTIONS_START__PROPS = any> = {
17
19
  /**
18
20
  * Key of the plugin. This is used to identify the plugin in the CMS.
@@ -28,8 +30,14 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
28
30
  * locales, specific for this plugin.
29
31
  * Keys can be existing `FireCMSTranslations` keys, or new keys if using
30
32
  * module augmentation.
33
+ *
34
+ * A value may be the strings themselves, or a function returning a promise
35
+ * of them. Use the function form for locales you ship in the plugin bundle:
36
+ * only the active language is ever read, so importing all of them statically
37
+ * puts every language into the bundle of every app that mounts the plugin.
38
+ * Both forms are supported; a plain record keeps working unchanged.
31
39
  */
32
- i18n?: Record<string, Record<string, string>>;
40
+ i18n?: Record<string, PluginTranslations | (() => Promise<PluginTranslations>)>;
33
41
  /**
34
42
  * You can use this prop to add higher order components to the CMS.
35
43
  * The components will be added to the root of the CMS, so any component
@@ -121,18 +129,24 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
121
129
  blockSearch?: (props: {
122
130
  context: FireCMSContext;
123
131
  path: string;
132
+ /** `path` split at its real segment boundaries, when known. */
133
+ pathSegments?: string[];
124
134
  collection: EC;
125
135
  parentCollectionIds?: string[];
126
136
  }) => boolean;
127
137
  showTextSearchBar?: (props: {
128
138
  context: FireCMSContext;
129
139
  path: string;
140
+ /** `path` split at its real segment boundaries, when known. */
141
+ pathSegments?: string[];
130
142
  collection: EC;
131
143
  parentCollectionIds?: string[];
132
144
  }) => boolean;
133
145
  onTextSearchClick?: (props: {
134
146
  context: FireCMSContext;
135
147
  path: string;
148
+ /** `path` split at its real segment boundaries, when known. */
149
+ pathSegments?: string[];
136
150
  collection: EC;
137
151
  parentCollectionIds?: string[];
138
152
  }) => Promise<boolean>;
@@ -258,6 +272,13 @@ export interface PluginHomePageActionsProps<EP extends object = object, M extend
258
272
  export interface PluginFormActionProps<USER extends User = User, EC extends EntityCollection = EntityCollection> {
259
273
  entityId?: string;
260
274
  path: string;
275
+ /**
276
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
277
+ *
278
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
279
+ * contain "/". Absent means "not known here", not "no slashes".
280
+ */
281
+ pathSegments?: string[];
261
282
  parentCollectionIds: string[];
262
283
  status: EntityStatus;
263
284
  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
  /**
@@ -56,8 +56,32 @@ export interface FireCMSTranslations {
56
56
  select_from: string;
57
57
  done: string;
58
58
  log_out: string;
59
- license_needed: string;
60
- license_description: string;
59
+ /** License banner during the PRO trial. `{{days}}` is the number of days left; plural form. */
60
+ license_trial_banner: string;
61
+ /** `license_trial_banner` when one day is left (i18next `_one` plural form). */
62
+ license_trial_banner_one: string;
63
+ /** License banner once the trial has ended. `{{date}}` is the day it ended. */
64
+ license_expired_banner: string;
65
+ /** `license_expired_banner` when the end date is not known. */
66
+ license_expired_banner_undated: string;
67
+ /** License banner when the license key is not linked to this project. `{{projectId}}` is the Firebase project id. */
68
+ license_invalid_project_banner: string;
69
+ /** `license_invalid_project_banner` when the project id is not known. */
70
+ license_invalid_project_banner_unnamed: string;
71
+ /** License banner when more projects are linked than the license covers. `{{licensed}}` covered, `{{actual}}` linked; plural form. */
72
+ license_over_quota_banner: string;
73
+ /** `license_over_quota_banner` when the license covers one project (i18next `_one` plural form). */
74
+ license_over_quota_banner_one: string;
75
+ /** Link in the license banner to buy a license. */
76
+ license_get_license: string;
77
+ /** Link in the over-quota banner to the license settings. */
78
+ license_update_license: string;
79
+ /** Link in the invalid-project banner to link this project to the license. */
80
+ license_add_project_to_license: string;
81
+ /** Title of the notice a PRO view shows while PRO features are paused. */
82
+ pro_features_paused: string;
83
+ /** Users and Roles views while PRO features are paused. */
84
+ user_management_paused_description: string;
61
85
  column_cannot_be_edited: string;
62
86
  close: string;
63
87
  unsaved_local_changes: string;
@@ -370,6 +394,10 @@ export interface FireCMSTranslations {
370
394
  include_undefined_values: string;
371
395
  /** Toggle in the export dialog, only shown when a filter or sort is active */
372
396
  export_apply_filter_sort: string;
397
+ /** Choice in the export dialog, only shown when entities are selected */
398
+ export_all_entities: string;
399
+ /** Same choice; `{{count}}` is the number of selected entities */
400
+ export_selected_entities: string;
373
401
  submit: string;
374
402
  no_filterable_properties: string;
375
403
  apply_filters: string;
@@ -415,6 +443,8 @@ export interface FireCMSTranslations {
415
443
  roles_menu: string;
416
444
  project_settings: string;
417
445
  firestore_manager: string;
446
+ /** The DataTalk product name, keyed so a locale can transliterate it. */
447
+ datatalk: string;
418
448
  manage_your_firestore_data: string;
419
449
  build_admin_panel_in_minutes: string;
420
450
  go_live_instantly: string;
@@ -429,6 +459,7 @@ export interface FireCMSTranslations {
429
459
  not_the_admin_panel: string;
430
460
  join_our_newsletter: string;
431
461
  by_signing_in_you_agree_to_our: string;
462
+ please_accept_the_terms_to_continue: string;
432
463
  terms_and_conditions: string;
433
464
  and_our: string;
434
465
  privacy_policy: string;
@@ -467,6 +498,8 @@ export interface FireCMSTranslations {
467
498
  licensed_projects_lowercase: string;
468
499
  manage: string;
469
500
  update_payment_method: string;
501
+ /** Snackbar when a Stripe billing portal link can't be created. `{{message}}` is the reason. */
502
+ stripe_portal_error: string;
470
503
  your_firecms_cloud_projects: string;
471
504
  status_active: string;
472
505
  status_trialing: string;
@@ -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,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.9408c41",
5
5
  "description": "Awesome Firebase/Firestore-based headless open-source CMS",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/firecmsco"
@@ -10,7 +10,7 @@
10
10
  "license": "MIT",
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "https://github.com/FireCMSco/firecms.git",
13
+ "url": "git+https://github.com/firecmsco/firecms.git",
14
14
  "directory": "packages/firecms_core"
15
15
  },
16
16
  "main": "./dist/index.umd.js",
@@ -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.9408c41",
57
+ "@firecms/ui": "3.4.0-canary.9408c41",
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,14 +89,13 @@
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
96
  "react-transition-group": "^4.4.5",
97
97
  "react-use-measure": "^2.1.7",
98
98
  "react-window": "^1.8.11",
99
- "vite-plugin-static-copy": "4.1.1",
100
99
  "yup": "^1.7.1"
101
100
  },
102
101
  "peerDependencies": {
@@ -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);
@@ -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 {...{
@@ -42,6 +42,8 @@ export type EntityCollectionBoardViewProps<M extends Record<string, any> = any>
42
42
  collection: EntityCollection<M>;
43
43
  tableController: EntityTableController<M>;
44
44
  fullPath: string;
45
+ /** `fullPath` split at its real segment boundaries; forwarded to the datasource. */
46
+ pathSegments?: string[];
45
47
  parentCollectionIds?: string[];
46
48
  columnProperty: string;
47
49
  onEntityClick?: (entity: Entity<M>) => void;
@@ -60,6 +62,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
60
62
  collection,
61
63
  tableController,
62
64
  fullPath,
65
+ pathSegments,
63
66
  parentCollectionIds = [],
64
67
  columnProperty,
65
68
  onEntityClick,
@@ -169,6 +172,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
169
172
  // Use the new per-column data controller
170
173
  const boardDataController = useBoardDataController<M>({
171
174
  fullPath,
175
+ pathSegments,
172
176
  collection,
173
177
  columnProperty,
174
178
  columns,
@@ -270,6 +274,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
270
274
 
271
275
  currentDataSource.countEntities({
272
276
  path: fullPath,
277
+ pathSegments,
273
278
  collection: currentCollection
274
279
  }).then(count => {
275
280
  totalCount = count;
@@ -281,6 +286,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
281
286
 
282
287
  currentDataSource.countEntities({
283
288
  path: fullPath,
289
+ pathSegments,
284
290
  collection: currentCollection,
285
291
  filter: { [orderProperty]: ["!=", null] } as FilterValues<string>
286
292
  }).then(count => {
@@ -432,6 +438,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
432
438
 
433
439
  const saveProps: SaveEntityProps = {
434
440
  path: entity.path,
441
+ // Paired with `entity.path`: the segments the entity was loaded with.
442
+ pathSegments: entity.pathSegments,
435
443
  entityId: entity.id,
436
444
  values: updatedValues as M,
437
445
  previousValues: entity.values,
@@ -472,6 +480,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
472
480
  console.log("Fetching all documents from collection...");
473
481
  const allDocs = await dataSource.fetchCollection<M>({
474
482
  path: fullPath,
483
+ pathSegments,
475
484
  collection,
476
485
  limit: 10000 // Fetch all
477
486
  });
@@ -492,6 +501,8 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
492
501
 
493
502
  const saveProps: SaveEntityProps = {
494
503
  path: entity.path,
504
+ // Paired with `entity.path`: the segments the entity was loaded with.
505
+ pathSegments: entity.pathSegments,
495
506
  entityId: entity.id,
496
507
  values: updatedValues as M,
497
508
  previousValues: entity.values,
@@ -663,6 +674,7 @@ export function EntityCollectionBoardView<M extends Record<string, any> = any>({
663
674
  });
664
675
  sideEntityController.open({
665
676
  path: fullPath,
677
+ pathSegments,
666
678
  collection,
667
679
  entityId: undefined,
668
680
  updateUrl: true,