@firecms/core 3.4.0-canary.f6a889a → 3.5.0

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 (192) hide show
  1. package/README.md +4 -0
  2. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  3. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  4. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  5. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
  6. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  7. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  8. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  9. package/dist/components/ErrorBoundary.d.ts +1 -3
  10. package/dist/components/LicenseBanner.d.ts +24 -0
  11. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  12. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  13. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  14. package/dist/components/common/table_url_params.d.ts +24 -0
  15. package/dist/components/common/useDataSourceTableController.d.ts +9 -1
  16. package/dist/components/common/useTableSearchHelper.d.ts +9 -1
  17. package/dist/components/index.d.ts +1 -0
  18. package/dist/contexts/LicenseStatusContext.d.ts +16 -0
  19. package/dist/core/EntityEditView.d.ts +9 -1
  20. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  21. package/dist/core/index.d.ts +1 -0
  22. package/dist/core/plugin_translations.d.ts +38 -0
  23. package/dist/core/pro_plugins.d.ts +40 -0
  24. package/dist/de-CBOjCvsy.js +707 -0
  25. package/dist/de-CBOjCvsy.js.map +1 -0
  26. package/dist/editor/extensions/Image.d.ts +11 -0
  27. package/dist/editor/index.d.ts +2 -1
  28. package/dist/editor/lazy_editor.d.ts +6 -0
  29. package/dist/editor-BSo_aS1I.js +4563 -0
  30. package/dist/editor-BSo_aS1I.js.map +1 -0
  31. package/dist/form/EntityForm.d.ts +8 -1
  32. package/dist/form/EntityFormActions.d.ts +2 -0
  33. package/dist/fr-DRfttcG0.js +707 -0
  34. package/dist/fr-DRfttcG0.js.map +1 -0
  35. package/dist/hi-DwieOZNf.js +707 -0
  36. package/dist/hi-DwieOZNf.js.map +1 -0
  37. package/dist/hooks/data/delete.d.ts +1 -1
  38. package/dist/hooks/data/save.d.ts +1 -1
  39. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  40. package/dist/hooks/data/useEntityFetch.d.ts +9 -1
  41. package/dist/hooks/index.d.ts +2 -0
  42. package/dist/hooks/useLicenseStatus.d.ts +16 -0
  43. package/dist/hooks/useProjectLog.d.ts +11 -6
  44. package/dist/hooks/useTranslation.d.ts +1 -1
  45. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  46. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  47. package/dist/index.es.js +4059 -11384
  48. package/dist/index.es.js.map +1 -1
  49. package/dist/index.umd.js +9090 -7610
  50. package/dist/index.umd.js.map +1 -1
  51. package/dist/it-l2KXtx9n.js +707 -0
  52. package/dist/it-l2KXtx9n.js.map +1 -0
  53. package/dist/pl-DPZrdKhi.js +709 -0
  54. package/dist/pl-DPZrdKhi.js.map +1 -0
  55. package/dist/pt-BoHpabw1.js +709 -0
  56. package/dist/pt-BoHpabw1.js.map +1 -0
  57. package/dist/types/analytics.d.ts +1 -1
  58. package/dist/types/collections.d.ts +14 -0
  59. package/dist/types/datasource.d.ts +67 -4
  60. package/dist/types/entities.d.ts +21 -1
  61. package/dist/types/entity_actions.d.ts +6 -0
  62. package/dist/types/entity_callbacks.d.ts +21 -0
  63. package/dist/types/firecms.d.ts +18 -1
  64. package/dist/types/firecms_context.d.ts +8 -0
  65. package/dist/types/index.d.ts +1 -0
  66. package/dist/types/license.d.ts +81 -0
  67. package/dist/types/navigation.d.ts +34 -5
  68. package/dist/types/plugins.d.ts +22 -1
  69. package/dist/types/properties.d.ts +17 -1
  70. package/dist/types/side_entity_controller.d.ts +7 -0
  71. package/dist/types/translations.d.ts +51 -2
  72. package/dist/util/__tests__/collections.test.d.ts +1 -0
  73. package/dist/util/__tests__/urls.test.d.ts +1 -0
  74. package/dist/util/collections.d.ts +1 -1
  75. package/dist/util/date_fns_locales.d.ts +18 -0
  76. package/dist/util/entity_cache.d.ts +11 -0
  77. package/dist/util/index.d.ts +2 -0
  78. package/dist/util/lazy_eager.d.ts +3 -3
  79. package/dist/util/local_storage.d.ts +23 -0
  80. package/dist/util/navigation_from_path.d.ts +7 -0
  81. package/dist/util/navigation_utils.d.ts +87 -3
  82. package/dist/util/parent_references_from_path.d.ts +16 -0
  83. package/dist/util/paths.d.ts +28 -0
  84. package/dist/util/permissions.d.ts +10 -4
  85. package/dist/util/urls.d.ts +22 -0
  86. package/package.json +5 -6
  87. package/src/app/Scaffold.tsx +7 -0
  88. package/src/components/DeleteEntityDialog.tsx +2 -0
  89. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  90. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +1 -0
  91. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  92. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  93. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  94. package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
  95. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  96. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -0
  97. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  98. package/src/components/EntityCollectionView/utils.ts +4 -4
  99. package/src/components/EntityPreview.tsx +4 -1
  100. package/src/components/ErrorBoundary.tsx +12 -18
  101. package/src/components/LicenseBanner.tsx +287 -0
  102. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  103. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  104. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  105. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  106. package/src/components/common/default_entity_actions.tsx +22 -2
  107. package/src/components/common/table_url_params.ts +172 -0
  108. package/src/components/common/useDataSourceTableController.tsx +86 -179
  109. package/src/components/common/useTableSearchHelper.ts +26 -2
  110. package/src/components/index.tsx +1 -0
  111. package/src/contexts/LicenseStatusContext.tsx +50 -0
  112. package/src/contexts/SnackbarProvider.tsx +14 -1
  113. package/src/core/DefaultAppBar.tsx +28 -16
  114. package/src/core/EntityEditView.tsx +20 -3
  115. package/src/core/EntityEditViewFormActions.tsx +3 -2
  116. package/src/core/EntitySidePanel.tsx +8 -4
  117. package/src/core/FireCMS.tsx +73 -47
  118. package/src/core/SideDialogs.tsx +44 -1
  119. package/src/core/index.tsx +1 -0
  120. package/src/core/plugin_translations.ts +87 -0
  121. package/src/core/pro_plugins.ts +79 -0
  122. package/src/editor/components/SlashCommandMenu.tsx +4 -1
  123. package/src/editor/extensions/Image.ts +17 -1
  124. package/src/editor/hooks/useProseMirror.ts +5 -0
  125. package/src/editor/index.ts +11 -1
  126. package/src/editor/lazy_editor.tsx +33 -0
  127. package/src/form/EntityForm.tsx +31 -10
  128. package/src/form/EntityFormActions.tsx +2 -0
  129. package/src/hooks/data/delete.ts +8 -0
  130. package/src/hooks/data/save.ts +6 -1
  131. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  132. package/src/hooks/data/useEntityFetch.tsx +35 -4
  133. package/src/hooks/index.tsx +2 -0
  134. package/src/hooks/useBuildLocalConfigurationPersistence.tsx +9 -9
  135. package/src/hooks/useBuildModeController.tsx +6 -4
  136. package/src/hooks/useBuildNavigationController.tsx +70 -15
  137. package/src/hooks/useFireCMSContext.tsx +7 -3
  138. package/src/hooks/useLicenseStatus.tsx +32 -0
  139. package/src/hooks/useProjectLog.tsx +27 -9
  140. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  141. package/src/hooks/useTranslation.ts +3 -1
  142. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  143. package/src/i18n/FireCMSi18nProvider.tsx +204 -33
  144. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  145. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  146. package/src/internal/useBuildDataSource.ts +37 -15
  147. package/src/internal/useBuildSideEntityController.tsx +19 -5
  148. package/src/locales/de.ts +28 -2
  149. package/src/locales/en.ts +33 -5
  150. package/src/locales/es.ts +28 -2
  151. package/src/locales/fr.ts +28 -2
  152. package/src/locales/hi.ts +28 -2
  153. package/src/locales/it.ts +28 -2
  154. package/src/locales/pl.ts +28 -2
  155. package/src/locales/pt.ts +28 -2
  156. package/src/preview/components/DatePreview.tsx +2 -4
  157. package/src/preview/components/ReferencePreview.tsx +7 -3
  158. package/src/preview/components/StorageThumbnail.tsx +24 -2
  159. package/src/routes/FireCMSRoute.tsx +11 -3
  160. package/src/types/analytics.ts +3 -0
  161. package/src/types/collections.ts +16 -0
  162. package/src/types/datasource.ts +67 -4
  163. package/src/types/entities.ts +24 -1
  164. package/src/types/entity_actions.tsx +6 -0
  165. package/src/types/entity_callbacks.ts +24 -0
  166. package/src/types/firecms.tsx +19 -1
  167. package/src/types/firecms_context.tsx +9 -0
  168. package/src/types/index.ts +1 -0
  169. package/src/types/license.ts +90 -0
  170. package/src/types/navigation.ts +35 -5
  171. package/src/types/plugins.tsx +23 -1
  172. package/src/types/properties.ts +17 -0
  173. package/src/types/side_entity_controller.tsx +7 -0
  174. package/src/types/translations.ts +51 -2
  175. package/src/util/__tests__/collections.test.ts +32 -0
  176. package/src/util/__tests__/urls.test.ts +131 -0
  177. package/src/util/collections.ts +1 -1
  178. package/src/util/date_fns_locales.ts +168 -0
  179. package/src/util/entities.ts +3 -1
  180. package/src/util/entity_cache.ts +28 -2
  181. package/src/util/index.ts +2 -0
  182. package/src/util/lazy_eager.tsx +30 -8
  183. package/src/util/local_storage.ts +52 -0
  184. package/src/util/navigation_from_path.ts +25 -2
  185. package/src/util/navigation_utils.ts +214 -3
  186. package/src/util/parent_references_from_path.ts +42 -3
  187. package/src/util/paths.ts +40 -3
  188. package/src/util/permissions.ts +22 -10
  189. package/src/util/previews.ts +8 -1
  190. package/src/util/resolutions.ts +8 -0
  191. package/src/util/urls.ts +52 -0
  192. package/src/util/useStorageUploadController.tsx +6 -1
@@ -0,0 +1,90 @@
1
+ /**
2
+ * What the FireCMS license check says about this project.
3
+ *
4
+ * - `not_required`: no PRO plugin is mounted.
5
+ * - `whitelisted`: the project is exempt from licensing.
6
+ * - `licensed`: a license covers this project.
7
+ * - `trial`: PRO plugins run on the free trial; see `daysLeft` and `trialEndsAt`.
8
+ * - `expired`: the trial ended without a license. PRO plugins are paused.
9
+ * - `invalid_project`: a license key was presented, but this project is not
10
+ * linked to that license. PRO plugins are paused.
11
+ * - `over_quota`: the license links more projects than it pays for, and this
12
+ * is not one of the projects it covers in production. PRO plugins are
13
+ * paused.
14
+ *
15
+ * @group Models
16
+ */
17
+ export type LicenseState =
18
+ | "not_required"
19
+ | "whitelisted"
20
+ | "licensed"
21
+ | "trial"
22
+ | "expired"
23
+ | "invalid_project"
24
+ | "over_quota";
25
+
26
+ /**
27
+ * Response of the FireCMS license check (`POST https://api.firecms.co/access_log`).
28
+ *
29
+ * Servers older than the PRO relaunch send only `blocked` and `message`, so
30
+ * treat a missing `licenseState` as unknown. `blocked` is honoured either way.
31
+ *
32
+ * @group Models
33
+ */
34
+ export type AccessResponse = {
35
+ /**
36
+ * True when the PRO plugins must be paused. The core CMS is never blocked.
37
+ */
38
+ blocked?: boolean;
39
+ licenseState?: LicenseState;
40
+ /**
41
+ * Number of projects the license pays for. Present with `licensed` and `over_quota`.
42
+ */
43
+ licensedProjects?: number;
44
+ /**
45
+ * Number of projects linked to the license.
46
+ */
47
+ linkedProjects?: number;
48
+ projectId?: string;
49
+ licenseId?: string;
50
+ /**
51
+ * ISO date the trial ends or ended. Present with `trial` and `expired`.
52
+ */
53
+ trialEndsAt?: string;
54
+ /**
55
+ * Whole days left in the trial, present with `trial`.
56
+ */
57
+ daysLeft?: number;
58
+ /**
59
+ * Where to get or update a license for this project.
60
+ */
61
+ subscribeUrl?: string;
62
+ /**
63
+ * A PRO plugin the server suggests for this project. Only read with
64
+ * `not_required`, and only shown when the app mounts no PRO plugin: the
65
+ * server also answers `not_required` to localhost and FireCMS Cloud,
66
+ * whatever they mount.
67
+ */
68
+ suggestion?: ProSuggestion;
69
+ message?: string;
70
+ };
71
+
72
+ /**
73
+ * A FireCMS PRO plugin the license check suggests to a project that runs
74
+ * none, such as user management for a CMS that several people sign in to.
75
+ * The server decides which projects get one; the CMS shows it as a note the
76
+ * user can dismiss for good.
77
+ *
78
+ * @group Models
79
+ */
80
+ export type ProSuggestion = {
81
+ /**
82
+ * Key of the suggested plugin. Keys this version of FireCMS has no copy
83
+ * for are ignored, so the server can add new ones safely.
84
+ */
85
+ plugin: "user_management" | "entity_history";
86
+ /**
87
+ * How many people use this CMS, when the server counted them.
88
+ */
89
+ users?: number;
90
+ };
@@ -57,8 +57,12 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
57
57
  /**
58
58
  * Get the collection configuration for a given path.
59
59
  * The collection is resolved from the given path or alias.
60
+ *
61
+ * Pass `pathSegments` when you have them: without them the path is split on "/" and
62
+ * asserted to have an odd number of parts, which a chain containing a slash-bearing
63
+ * entity id fails — it throws instead of resolving.
60
64
  */
61
- getCollection: (pathOrId: string, includeUserOverride?: boolean) => EC | undefined;
65
+ getCollection: (pathOrId: string, includeUserOverride?: boolean, pathSegments?: string[]) => EC | undefined;
62
66
 
63
67
  /**
64
68
  * Get the top level collection configuration for a given id
@@ -118,9 +122,30 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
118
122
  /**
119
123
  * Turn a path with collection ids into a resolved path.
120
124
  * The ids (typically used in urls) will be replaced with relative paths (typically used in database paths)
125
+ *
126
+ * Pass `pathSegments` whenever you have them. Without them this has to find the entity
127
+ * ids inside the flattened string by reading up to the next "/", which is wrong for any
128
+ * id that contains one — the chain then shifts by a segment and resolution fails.
129
+ *
121
130
  * @param pathWithAliases
131
+ * @param pathSegments `pathWithAliases` split at its real segment boundaries, if known
122
132
  */
123
- resolveIdsFrom: (pathWithAliases: string) => string;
133
+ resolveIdsFrom: (pathWithAliases: string, pathSegments?: string[]) => string;
134
+
135
+ /**
136
+ * The segment-wise counterpart of {@link resolveIdsFrom}: replaces collection ids with
137
+ * their real paths while carrying every entity id through whole, slashes included.
138
+ *
139
+ * Idempotent — segments that are already resolved pass through unchanged.
140
+ *
141
+ * Optional so that a `navigationController` built against an earlier version keeps
142
+ * working: callers fall back to using the segments as given, which is what happened
143
+ * before this method existed. Controllers built by `useBuildNavigationController`
144
+ * always provide it.
145
+ *
146
+ * @param pathSegments
147
+ */
148
+ resolveSegmentsFrom?: (pathSegments: string[]) => string[];
124
149
 
125
150
  /**
126
151
  * Call this method to recalculate the navigation
@@ -128,16 +153,21 @@ export type NavigationController<EC extends EntityCollection = EntityCollection<
128
153
  refreshNavigation: () => void;
129
154
 
130
155
  /**
131
- * Retrieve all the related parent references for a given path
156
+ * Retrieve all the related parent references for a given path.
157
+ *
158
+ * `path` is expected escaped, as it comes from the URL. Pass `pathSegments` when the
159
+ * path at hand is a raw datasource path, whose ids may contain a bare "/".
160
+ *
132
161
  * @param path
162
+ * @param pathSegments `path` split at its real segment boundaries, with raw ids
133
163
  */
134
- getParentReferencesFromPath: (path: string) => EntityReference[];
164
+ getParentReferencesFromPath: (path: string, pathSegments?: string[]) => EntityReference[];
135
165
 
136
166
  /**
137
167
  * Retrieve all the related parent collection ids for a given path
138
168
  * @param path
139
169
  */
140
- getParentCollectionIds: (path: string) => string[];
170
+ getParentCollectionIds: (path: string, pathSegments?: string[]) => string[];
141
171
 
142
172
  /**
143
173
  * Resolve paths from a list of ids
@@ -15,6 +15,9 @@ import { InternalUserManagement } from "./internal_user_management";
15
15
  * NOTE: This is a work in progress and the API is not stable yet.
16
16
  * @group Core
17
17
  */
18
+ /** The strings a plugin contributes for one locale. */
19
+ export type PluginTranslations = Record<string, string>;
20
+
18
21
  export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollection = EntityCollection, COL_ACTIONS_PROPS = any, COL_ACTIONS_START__PROPS = any> = {
19
22
 
20
23
  /**
@@ -33,8 +36,14 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
33
36
  * locales, specific for this plugin.
34
37
  * Keys can be existing `FireCMSTranslations` keys, or new keys if using
35
38
  * module augmentation.
39
+ *
40
+ * A value may be the strings themselves, or a function returning a promise
41
+ * of them. Use the function form for locales you ship in the plugin bundle:
42
+ * only the active language is ever read, so importing all of them statically
43
+ * puts every language into the bundle of every app that mounts the plugin.
44
+ * Both forms are supported; a plain record keeps working unchanged.
36
45
  */
37
- i18n?: Record<string, Record<string, string>>;
46
+ i18n?: Record<string, PluginTranslations | (() => Promise<PluginTranslations>)>;
38
47
 
39
48
  /**
40
49
  * You can use this prop to add higher order components to the CMS.
@@ -146,6 +155,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
146
155
  blockSearch?: (props: {
147
156
  context: FireCMSContext,
148
157
  path: string,
158
+ /** `path` split at its real segment boundaries, when known. */
159
+ pathSegments?: string[],
149
160
  collection: EC,
150
161
  parentCollectionIds?: string[]
151
162
  }) => boolean;
@@ -153,6 +164,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
153
164
  showTextSearchBar?: (props: {
154
165
  context: FireCMSContext,
155
166
  path: string,
167
+ /** `path` split at its real segment boundaries, when known. */
168
+ pathSegments?: string[],
156
169
  collection: EC,
157
170
  parentCollectionIds?: string[]
158
171
  }) => boolean;
@@ -160,6 +173,8 @@ export type FireCMSPlugin<PROPS = any, FORM_PROPS = any, EC extends EntityCollec
160
173
  onTextSearchClick?: (props: {
161
174
  context: FireCMSContext,
162
175
  path: string,
176
+ /** `path` split at its real segment boundaries, when known. */
177
+ pathSegments?: string[],
163
178
  collection: EC,
164
179
  parentCollectionIds?: string[]
165
180
  }) => Promise<boolean>;
@@ -308,6 +323,13 @@ export interface PluginHomePageActionsProps<EP extends object = object, M extend
308
323
  export interface PluginFormActionProps<USER extends User = User, EC extends EntityCollection = EntityCollection> {
309
324
  entityId?: string;
310
325
  path: string;
326
+ /**
327
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
328
+ *
329
+ * Optional, and never derived by splitting `path` — a parent entity id may itself
330
+ * contain "/". Absent means "not known here", not "no slashes".
331
+ */
332
+ pathSegments?: string[];
311
333
  parentCollectionIds: string[];
312
334
  status: EntityStatus;
313
335
  collection: EC;
@@ -562,6 +562,22 @@ export type PropertyBuilderProps<M extends Record<string, any> = any> =
562
562
  * Current value of this property
563
563
  */
564
564
  propertyValue?: any;
565
+ /**
566
+ * Path of this property within the entity, in dot notation, e.g.
567
+ * `my_map.my_field` or `my_array.0.my_field`.
568
+ * Useful to look up sibling values in `values` with `getValueInPath`.
569
+ *
570
+ * When a property builder is used as the `of` prop of an array property,
571
+ * it is resolved twice: once per existing element, with an index
572
+ * qualified key (`my_array.0`), and once as the template for the element
573
+ * shape, with the array's own key (`my_array`). In the latter case
574
+ * `propertyValue` is the whole array, since the template is not bound to
575
+ * any element.
576
+ *
577
+ * It may be undefined if the property is resolved outside the context of
578
+ * an entity.
579
+ */
580
+ propertyKey?: string;
565
581
  /**
566
582
  * Index of this property (only for arrays)
567
583
  */
@@ -592,6 +608,7 @@ export type PropertyBuilder<T extends CMSType = any, M extends Record<string, an
592
608
  values,
593
609
  previousValues,
594
610
  propertyValue,
611
+ propertyKey,
595
612
  index,
596
613
  path,
597
614
  entityId,
@@ -19,6 +19,13 @@ export interface EntitySidePanelProps<M extends Record<string, any> = any> {
19
19
  * Full CMS path of the entity, including the collection and sub-collections.
20
20
  */
21
21
  fullIdPath?: string;
22
+ /**
23
+ * `path` split at its real segment boundaries, e.g. `["nodes", "node/42", "edges"]`.
24
+ *
25
+ * Optional, and never derived by splitting `path` — a guess would be wrong in exactly
26
+ * the case the field exists for. Absent means "not known here", not "no slashes".
27
+ */
28
+ pathSegments?: string[];
22
29
 
23
30
  /**
24
31
  * ID of the entity, if not set, it means we are creating a new entity
@@ -66,8 +66,40 @@ export interface FireCMSTranslations {
66
66
  select_from: string;
67
67
  done: string;
68
68
  log_out: string;
69
- license_needed: string;
70
- license_description: string;
69
+ /** License banner during the PRO trial. `{{days}}` is the number of days left; plural form. */
70
+ license_trial_banner: string;
71
+ /** `license_trial_banner` when one day is left (i18next `_one` plural form). */
72
+ license_trial_banner_one: string;
73
+ /** License banner once the trial has ended. `{{date}}` is the day it ended. */
74
+ license_expired_banner: string;
75
+ /** `license_expired_banner` when the end date is not known. */
76
+ license_expired_banner_undated: string;
77
+ /** License banner when the license key is not linked to this project. `{{projectId}}` is the Firebase project id. */
78
+ license_invalid_project_banner: string;
79
+ /** `license_invalid_project_banner` when the project id is not known. */
80
+ license_invalid_project_banner_unnamed: string;
81
+ /** License banner when more projects are linked than the license covers. `{{licensed}}` covered, `{{actual}}` linked; plural form. */
82
+ license_over_quota_banner: string;
83
+ /** `license_over_quota_banner` when the license covers one project (i18next `_one` plural form). */
84
+ license_over_quota_banner_one: string;
85
+ /** Link in the license banner to buy a license. */
86
+ license_get_license: string;
87
+ /** Link in the over-quota banner to the license settings. */
88
+ license_update_license: string;
89
+ /** Link in the invalid-project banner to link this project to the license. */
90
+ license_add_project_to_license: string;
91
+ /** Suggestion of the PRO user management plugin to a project without it. `{{users}}` is how many people use the CMS, always 2 or more. */
92
+ pro_suggestion_user_management: string;
93
+ /** `pro_suggestion_user_management` when the number of people is not known. */
94
+ pro_suggestion_user_management_uncounted: string;
95
+ /** Suggestion of the PRO entity history plugin to a project without it. */
96
+ pro_suggestion_entity_history: string;
97
+ /** Link in a PRO suggestion to the plugin's documentation. */
98
+ pro_suggestion_learn_more: string;
99
+ /** Title of the notice a PRO view shows while PRO features are paused. */
100
+ pro_features_paused: string;
101
+ /** Users and Roles views while PRO features are paused. */
102
+ user_management_paused_description: string;
71
103
  column_cannot_be_edited: string;
72
104
  close: string;
73
105
  unsaved_local_changes: string;
@@ -419,6 +451,14 @@ export interface FireCMSTranslations {
419
451
  download: string;
420
452
  large_number_of_documents: string;
421
453
  include_undefined_values: string;
454
+ /** Toggle in the export dialog, only shown when a filter or sort is active */
455
+ export_apply_filter_sort: string;
456
+ /** Choice in the export dialog, only shown when entities are selected */
457
+ export_all_entities: string;
458
+ /** Same choice; `{{count}}` is the number of selected entities */
459
+ export_selected_entities: string;
460
+ /** Shown in the export dialog when the export fails; `{{message}}` is the error's message */
461
+ export_error: string;
422
462
  submit: string;
423
463
 
424
464
  no_filterable_properties: string;
@@ -468,6 +508,8 @@ export interface FireCMSTranslations {
468
508
  roles_menu: string;
469
509
  project_settings: string;
470
510
  firestore_manager: string;
511
+ /** The DataTalk product name, keyed so a locale can transliterate it. */
512
+ datatalk: string;
471
513
  manage_your_firestore_data: string;
472
514
 
473
515
  // ─── FireCMS Cloud Login ──────────────────────────────────────
@@ -484,6 +526,7 @@ export interface FireCMSTranslations {
484
526
  not_the_admin_panel: string;
485
527
  join_our_newsletter: string;
486
528
  by_signing_in_you_agree_to_our: string;
529
+ please_accept_the_terms_to_continue: string;
487
530
  terms_and_conditions: string;
488
531
  and_our: string;
489
532
  privacy_policy: string;
@@ -526,6 +569,8 @@ export interface FireCMSTranslations {
526
569
  licensed_projects_lowercase: string;
527
570
  manage: string;
528
571
  update_payment_method: string;
572
+ /** Snackbar when a Stripe billing portal link can't be created. `{{message}}` is the reason. */
573
+ stripe_portal_error: string;
529
574
  your_firecms_cloud_projects: string;
530
575
  status_active: string;
531
576
  status_trialing: string;
@@ -620,6 +665,10 @@ export interface FireCMSTranslations {
620
665
  order_property_not_found: string;
621
666
  no_number_properties: string;
622
667
  order_property_description: string;
668
+ image_property: string;
669
+ image_property_not_found: string;
670
+ no_image_properties: string;
671
+ image_property_description: string;
623
672
  display_settings: string;
624
673
  default_row_size: string;
625
674
  side_dialog_width: string;
@@ -0,0 +1,32 @@
1
+ import { getLocalChangesBackup } from "../collections";
2
+ import { EntityCollection } from "../../types";
3
+
4
+ function collectionWith(localChangesBackup?: "manual_apply" | "auto_apply" | false): EntityCollection {
5
+ return {
6
+ id: "products",
7
+ name: "Products",
8
+ path: "products",
9
+ properties: {},
10
+ localChangesBackup
11
+ } as EntityCollection;
12
+ }
13
+
14
+ describe("getLocalChangesBackup", () => {
15
+
16
+ it("should default to manual_apply when not set", () => {
17
+ expect(getLocalChangesBackup(collectionWith(undefined))).toEqual("manual_apply");
18
+ });
19
+
20
+ it("should return manual_apply when explicitly set", () => {
21
+ expect(getLocalChangesBackup(collectionWith("manual_apply"))).toEqual("manual_apply");
22
+ });
23
+
24
+ it("should return auto_apply when explicitly set", () => {
25
+ expect(getLocalChangesBackup(collectionWith("auto_apply"))).toEqual("auto_apply");
26
+ });
27
+
28
+ it("should return false when explicitly disabled, and not fall back to the default", () => {
29
+ expect(getLocalChangesBackup(collectionWith(false))).toEqual(false);
30
+ });
31
+
32
+ });
@@ -0,0 +1,131 @@
1
+ import { getFileExtensionFromUrl, getPreviewTypeFromUrl, isAbsoluteHttpUrl } from "../urls";
2
+
3
+ describe("url utilities", () => {
4
+
5
+ describe("isAbsoluteHttpUrl", () => {
6
+
7
+ it("should detect absolute http and https URLs", () => {
8
+ expect(isAbsoluteHttpUrl("https://abc.com/abc.png")).toBe(true);
9
+ expect(isAbsoluteHttpUrl("http://abc.com/abc.png")).toBe(true);
10
+ expect(isAbsoluteHttpUrl("HTTPS://ABC.COM/ABC.PNG")).toBe(true);
11
+ });
12
+
13
+ it("should detect Firebase Storage download URLs as absolute URLs", () => {
14
+ expect(isAbsoluteHttpUrl("https://firebasestorage.googleapis.com/v0/b/demo.appspot.com/o/images%2Fcat.png?alt=media&token=abc")).toBe(true);
15
+ expect(isAbsoluteHttpUrl("https://storage.googleapis.com/demo.appspot.com/images/cat.png")).toBe(true);
16
+ });
17
+
18
+ it("should not consider storage paths absolute URLs", () => {
19
+ expect(isAbsoluteHttpUrl("images/cat.png")).toBe(false);
20
+ expect(isAbsoluteHttpUrl("/images/cat.png")).toBe(false);
21
+ expect(isAbsoluteHttpUrl("cat.png")).toBe(false);
22
+ });
23
+
24
+ it("should not consider other protocols absolute http URLs", () => {
25
+ expect(isAbsoluteHttpUrl("gs://demo.appspot.com/images/cat.png")).toBe(false);
26
+ expect(isAbsoluteHttpUrl("data:image/png;base64,AAAA")).toBe(false);
27
+ expect(isAbsoluteHttpUrl("//abc.com/abc.png")).toBe(false);
28
+ });
29
+
30
+ it("should handle empty values", () => {
31
+ expect(isAbsoluteHttpUrl("")).toBe(false);
32
+ expect(isAbsoluteHttpUrl(undefined)).toBe(false);
33
+ expect(isAbsoluteHttpUrl(null)).toBe(false);
34
+ });
35
+
36
+ it("should not match http appearing later in the value", () => {
37
+ expect(isAbsoluteHttpUrl("images/https://abc.com/abc.png")).toBe(false);
38
+ });
39
+
40
+ });
41
+
42
+ describe("getFileExtensionFromUrl", () => {
43
+
44
+ it("should extract the extension of a plain URL", () => {
45
+ expect(getFileExtensionFromUrl("https://abc.com/abc.png")).toBe("png");
46
+ });
47
+
48
+ it("should lowercase the extension", () => {
49
+ expect(getFileExtensionFromUrl("https://abc.com/ABC.PNG")).toBe("png");
50
+ });
51
+
52
+ it("should ignore query strings and hash fragments", () => {
53
+ expect(getFileExtensionFromUrl("https://abc.com/abc.png?width=200")).toBe("png");
54
+ expect(getFileExtensionFromUrl("https://abc.com/abc.png#anchor")).toBe("png");
55
+ expect(getFileExtensionFromUrl("https://firebasestorage.googleapis.com/v0/b/demo.appspot.com/o/images%2Fcat.jpeg?alt=media&token=abc")).toBe("jpeg");
56
+ });
57
+
58
+ it("should work with storage paths", () => {
59
+ expect(getFileExtensionFromUrl("images/cat.webp")).toBe("webp");
60
+ });
61
+
62
+ it("should use the last dot of the file name", () => {
63
+ expect(getFileExtensionFromUrl("https://abc.com/archive.tar.gz")).toBe("gz");
64
+ });
65
+
66
+ it("should return undefined when there is no extension", () => {
67
+ expect(getFileExtensionFromUrl("https://abc.com/abc")).toBeUndefined();
68
+ expect(getFileExtensionFromUrl("https://abc.com/")).toBeUndefined();
69
+ expect(getFileExtensionFromUrl("images/cat")).toBeUndefined();
70
+ });
71
+
72
+ it("should not take dots of the host or the path as extensions", () => {
73
+ expect(getFileExtensionFromUrl("https://abc.com/images/cat")).toBeUndefined();
74
+ expect(getFileExtensionFromUrl("https://abc.com/abc.png/download")).toBeUndefined();
75
+ });
76
+
77
+ it("should return undefined for dotfiles and trailing dots", () => {
78
+ expect(getFileExtensionFromUrl("https://abc.com/.gitignore")).toBeUndefined();
79
+ expect(getFileExtensionFromUrl("https://abc.com/abc.")).toBeUndefined();
80
+ });
81
+
82
+ it("should handle empty values", () => {
83
+ expect(getFileExtensionFromUrl("")).toBeUndefined();
84
+ expect(getFileExtensionFromUrl(undefined)).toBeUndefined();
85
+ expect(getFileExtensionFromUrl(null)).toBeUndefined();
86
+ });
87
+
88
+ });
89
+
90
+ describe("getPreviewTypeFromUrl", () => {
91
+
92
+ it("should infer image previews", () => {
93
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.png")).toBe("image");
94
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.jpg")).toBe("image");
95
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.jpeg")).toBe("image");
96
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.gif")).toBe("image");
97
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.webp")).toBe("image");
98
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.svg")).toBe("image");
99
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.avif")).toBe("image");
100
+ });
101
+
102
+ it("should infer video previews", () => {
103
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.mp4")).toBe("video");
104
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.webm")).toBe("video");
105
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.mov")).toBe("video");
106
+ });
107
+
108
+ it("should infer audio previews", () => {
109
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.mp3")).toBe("audio");
110
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.wav")).toBe("audio");
111
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.m4a")).toBe("audio");
112
+ });
113
+
114
+ it("should infer the type of Firebase Storage download URLs", () => {
115
+ expect(getPreviewTypeFromUrl("https://firebasestorage.googleapis.com/v0/b/demo.appspot.com/o/images%2Fcat.png?alt=media&token=abc")).toBe("image");
116
+ });
117
+
118
+ it("should be case insensitive", () => {
119
+ expect(getPreviewTypeFromUrl("https://abc.com/ABC.PNG")).toBe("image");
120
+ });
121
+
122
+ it("should return undefined for unknown or missing extensions", () => {
123
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.pdf")).toBeUndefined();
124
+ expect(getPreviewTypeFromUrl("https://abc.com/abc.zip")).toBeUndefined();
125
+ expect(getPreviewTypeFromUrl("https://abc.com/abc")).toBeUndefined();
126
+ expect(getPreviewTypeFromUrl(undefined)).toBeUndefined();
127
+ });
128
+
129
+ });
130
+
131
+ });
@@ -117,7 +117,7 @@ export const applyPermissionsFunctionIfEmpty = (collections: EntityCollection[],
117
117
  }
118
118
 
119
119
  export function getLocalChangesBackup(collection: EntityCollection) {
120
- if (!collection.localChangesBackup) {
120
+ if (collection.localChangesBackup === undefined) {
121
121
  return "manual_apply";
122
122
  }
123
123