@firecms/core 3.3.0 → 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 (220) hide show
  1. package/README.md +4 -0
  2. package/dist/app/useApp.d.ts +1 -0
  3. package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
  4. package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
  5. package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
  6. package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
  7. package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
  8. package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
  9. package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
  10. package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
  11. package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
  12. package/dist/components/EntityPreview.d.ts +3 -1
  13. package/dist/components/ErrorBoundary.d.ts +1 -3
  14. package/dist/components/LicenseBanner.d.ts +23 -0
  15. package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
  16. package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
  17. package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
  18. package/dist/components/common/table_url_params.d.ts +24 -0
  19. package/dist/components/common/useDataSourceTableController.d.ts +9 -1
  20. package/dist/components/common/useTableSearchHelper.d.ts +9 -1
  21. package/dist/components/index.d.ts +1 -0
  22. package/dist/contexts/LicenseStatusContext.d.ts +16 -0
  23. package/dist/core/DefaultDrawer.d.ts +10 -3
  24. package/dist/core/EntityEditView.d.ts +9 -1
  25. package/dist/core/EntityEditViewFormActions.d.ts +1 -1
  26. package/dist/core/field_configs.d.ts +1 -1
  27. package/dist/core/index.d.ts +1 -0
  28. package/dist/core/plugin_translations.d.ts +38 -0
  29. package/dist/core/pro_plugins.d.ts +40 -0
  30. package/dist/de-KtBROUvS.js +700 -0
  31. package/dist/de-KtBROUvS.js.map +1 -0
  32. package/dist/editor/extensions/Image.d.ts +11 -0
  33. package/dist/editor/index.d.ts +2 -1
  34. package/dist/editor/lazy_editor.d.ts +6 -0
  35. package/dist/editor-BSo_aS1I.js +4563 -0
  36. package/dist/editor-BSo_aS1I.js.map +1 -0
  37. package/dist/form/EntityForm.d.ts +8 -1
  38. package/dist/form/EntityFormActions.d.ts +2 -0
  39. package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
  40. package/dist/form/index.d.ts +1 -0
  41. package/dist/fr-BRFSBaVh.js +700 -0
  42. package/dist/fr-BRFSBaVh.js.map +1 -0
  43. package/dist/hi-DwCkga5X.js +700 -0
  44. package/dist/hi-DwCkga5X.js.map +1 -0
  45. package/dist/hooks/data/delete.d.ts +1 -1
  46. package/dist/hooks/data/save.d.ts +1 -1
  47. package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
  48. package/dist/hooks/data/useEntityFetch.d.ts +9 -1
  49. package/dist/hooks/index.d.ts +2 -0
  50. package/dist/hooks/useLicenseStatus.d.ts +16 -0
  51. package/dist/hooks/useProjectLog.d.ts +11 -6
  52. package/dist/hooks/useTranslation.d.ts +1 -1
  53. package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
  54. package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
  55. package/dist/index.es.js +4509 -10665
  56. package/dist/index.es.js.map +1 -1
  57. package/dist/index.umd.js +9378 -6771
  58. package/dist/index.umd.js.map +1 -1
  59. package/dist/it-B2YGgDsx.js +700 -0
  60. package/dist/it-B2YGgDsx.js.map +1 -0
  61. package/dist/locales/pl.d.ts +2 -0
  62. package/dist/pl-BxHkywK8.js +702 -0
  63. package/dist/pl-BxHkywK8.js.map +1 -0
  64. package/dist/preview/index.d.ts +1 -0
  65. package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
  66. package/dist/pt-q6UV4diT.js +702 -0
  67. package/dist/pt-q6UV4diT.js.map +1 -0
  68. package/dist/types/analytics.d.ts +1 -1
  69. package/dist/types/collections.d.ts +31 -0
  70. package/dist/types/datasource.d.ts +67 -4
  71. package/dist/types/entities.d.ts +21 -1
  72. package/dist/types/entity_actions.d.ts +6 -0
  73. package/dist/types/entity_callbacks.d.ts +21 -0
  74. package/dist/types/firecms.d.ts +18 -1
  75. package/dist/types/firecms_context.d.ts +8 -0
  76. package/dist/types/index.d.ts +1 -0
  77. package/dist/types/license.d.ts +54 -0
  78. package/dist/types/navigation.d.ts +34 -5
  79. package/dist/types/plugins.d.ts +22 -1
  80. package/dist/types/properties.d.ts +17 -1
  81. package/dist/types/side_entity_controller.d.ts +7 -0
  82. package/dist/types/translations.d.ts +37 -2
  83. package/dist/util/__tests__/collections.test.d.ts +1 -0
  84. package/dist/util/__tests__/urls.test.d.ts +1 -0
  85. package/dist/util/collections.d.ts +1 -1
  86. package/dist/util/date_fns_locales.d.ts +18 -0
  87. package/dist/util/entities.d.ts +1 -0
  88. package/dist/util/entity_cache.d.ts +11 -0
  89. package/dist/util/geopoint.d.ts +22 -0
  90. package/dist/util/index.d.ts +3 -0
  91. package/dist/util/lazy_eager.d.ts +3 -3
  92. package/dist/util/navigation_blocking.d.ts +22 -0
  93. package/dist/util/navigation_from_path.d.ts +17 -0
  94. package/dist/util/navigation_utils.d.ts +107 -3
  95. package/dist/util/parent_references_from_path.d.ts +16 -0
  96. package/dist/util/paths.d.ts +28 -0
  97. package/dist/util/permissions.d.ts +10 -4
  98. package/dist/util/urls.d.ts +22 -0
  99. package/package.json +17 -10
  100. package/src/app/Scaffold.tsx +41 -44
  101. package/src/app/useApp.tsx +1 -0
  102. package/src/components/DeleteEntityDialog.tsx +2 -0
  103. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
  104. package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
  105. package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
  106. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
  107. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
  108. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
  109. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
  110. package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
  111. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
  112. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -1
  113. package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
  114. package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
  115. package/src/components/EntityPreview.tsx +45 -41
  116. package/src/components/ErrorBoundary.tsx +12 -18
  117. package/src/components/LicenseBanner.tsx +209 -0
  118. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  119. package/src/components/ReferenceWidget.tsx +1 -1
  120. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  121. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  122. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  123. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  124. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  125. package/src/components/common/default_entity_actions.tsx +22 -2
  126. package/src/components/common/table_url_params.ts +172 -0
  127. package/src/components/common/useDataSourceTableController.tsx +126 -163
  128. package/src/components/common/useTableSearchHelper.ts +26 -2
  129. package/src/components/index.tsx +1 -0
  130. package/src/contexts/LicenseStatusContext.tsx +50 -0
  131. package/src/contexts/SnackbarProvider.tsx +14 -1
  132. package/src/core/DefaultAppBar.tsx +28 -16
  133. package/src/core/DefaultDrawer.tsx +150 -64
  134. package/src/core/DrawerNavigationGroup.tsx +27 -29
  135. package/src/core/DrawerNavigationItem.tsx +10 -12
  136. package/src/core/EntityEditView.tsx +77 -35
  137. package/src/core/EntityEditViewFormActions.tsx +3 -2
  138. package/src/core/EntitySidePanel.tsx +8 -4
  139. package/src/core/FireCMS.tsx +73 -47
  140. package/src/core/SideDialogs.tsx +44 -1
  141. package/src/core/field_configs.tsx +15 -0
  142. package/src/core/index.tsx +1 -0
  143. package/src/core/plugin_translations.ts +87 -0
  144. package/src/core/pro_plugins.ts +79 -0
  145. package/src/editor/components/SlashCommandMenu.tsx +4 -1
  146. package/src/editor/extensions/Image.ts +17 -1
  147. package/src/editor/hooks/useProseMirror.ts +5 -0
  148. package/src/editor/index.ts +11 -1
  149. package/src/editor/lazy_editor.tsx +33 -0
  150. package/src/form/EntityForm.tsx +31 -10
  151. package/src/form/EntityFormActions.tsx +2 -0
  152. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  153. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  154. package/src/form/index.tsx +1 -0
  155. package/src/hooks/data/delete.ts +8 -0
  156. package/src/hooks/data/save.ts +6 -1
  157. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  158. package/src/hooks/data/useEntityFetch.tsx +35 -4
  159. package/src/hooks/index.tsx +2 -0
  160. package/src/hooks/useBuildNavigationController.tsx +70 -15
  161. package/src/hooks/useFireCMSContext.tsx +7 -3
  162. package/src/hooks/useLicenseStatus.tsx +32 -0
  163. package/src/hooks/useProjectLog.tsx +27 -9
  164. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  165. package/src/hooks/useTranslation.ts +3 -1
  166. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  167. package/src/i18n/FireCMSi18nProvider.tsx +199 -23
  168. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  169. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  170. package/src/internal/useBuildDataSource.ts +37 -15
  171. package/src/internal/useBuildSideEntityController.tsx +20 -5
  172. package/src/locales/de.ts +23 -4
  173. package/src/locales/en.ts +25 -4
  174. package/src/locales/es.ts +23 -4
  175. package/src/locales/fr.ts +23 -4
  176. package/src/locales/hi.ts +23 -4
  177. package/src/locales/it.ts +23 -4
  178. package/src/locales/pl.ts +749 -0
  179. package/src/locales/pt.ts +23 -4
  180. package/src/preview/PropertyPreview.tsx +12 -0
  181. package/src/preview/components/DatePreview.tsx +2 -4
  182. package/src/preview/components/ReferencePreview.tsx +7 -3
  183. package/src/preview/components/StorageThumbnail.tsx +24 -2
  184. package/src/preview/index.ts +1 -0
  185. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  186. package/src/routes/FireCMSRoute.tsx +54 -27
  187. package/src/types/analytics.ts +3 -0
  188. package/src/types/collections.ts +34 -0
  189. package/src/types/datasource.ts +67 -4
  190. package/src/types/entities.ts +24 -1
  191. package/src/types/entity_actions.tsx +6 -0
  192. package/src/types/entity_callbacks.ts +24 -0
  193. package/src/types/firecms.tsx +19 -1
  194. package/src/types/firecms_context.tsx +9 -0
  195. package/src/types/index.ts +1 -0
  196. package/src/types/license.ts +62 -0
  197. package/src/types/navigation.ts +35 -5
  198. package/src/types/plugins.tsx +23 -1
  199. package/src/types/properties.ts +17 -0
  200. package/src/types/side_entity_controller.tsx +7 -0
  201. package/src/types/translations.ts +37 -2
  202. package/src/util/__tests__/collections.test.ts +32 -0
  203. package/src/util/__tests__/urls.test.ts +131 -0
  204. package/src/util/collections.ts +1 -1
  205. package/src/util/date_fns_locales.ts +168 -0
  206. package/src/util/entities.ts +16 -1
  207. package/src/util/entity_cache.ts +18 -0
  208. package/src/util/geopoint.ts +81 -0
  209. package/src/util/index.ts +3 -0
  210. package/src/util/lazy_eager.tsx +30 -8
  211. package/src/util/navigation_blocking.ts +45 -0
  212. package/src/util/navigation_from_path.ts +47 -7
  213. package/src/util/navigation_utils.ts +244 -4
  214. package/src/util/parent_references_from_path.ts +45 -4
  215. package/src/util/paths.ts +40 -3
  216. package/src/util/permissions.ts +22 -10
  217. package/src/util/previews.ts +8 -1
  218. package/src/util/resolutions.ts +8 -0
  219. package/src/util/urls.ts +52 -0
  220. package/src/util/useStorageUploadController.tsx +6 -1
@@ -0,0 +1,91 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import React from "react";
5
+ import { render, screen } from "@testing-library/react";
6
+ import { useTranslation } from "react-i18next";
7
+ import { FireCMSi18nProvider } from "../FireCMSi18nProvider";
8
+
9
+ /** Reads a key through the same channel application code uses. */
10
+ function Str({ k }: { k: string }) {
11
+ const { t } = useTranslation("firecms_core");
12
+ return <span data-testid={k}>{t(k)}</span>;
13
+ }
14
+
15
+ const HOST_TRANSLATIONS = {
16
+ en: {
17
+ host_only_key: "Explore your Firestore data",
18
+ save: "Publish" // an override of a real core key
19
+ }
20
+ } as any;
21
+
22
+ describe("FireCMSi18nProvider", () => {
23
+
24
+ beforeEach(() => {
25
+ window.localStorage.clear();
26
+ });
27
+
28
+ it("resolves its own translations", () => {
29
+ render(
30
+ <FireCMSi18nProvider translations={HOST_TRANSLATIONS}>
31
+ <Str k={"host_only_key"}/>
32
+ </FireCMSi18nProvider>
33
+ );
34
+ expect(screen.getByTestId("host_only_key").textContent).toBe("Explore your Firestore data");
35
+ });
36
+
37
+ it("keeps the host's translations when a nested provider is mounted without any", () => {
38
+ // This is the FireCMSCloudApp case: it mounts its own provider with
39
+ // `translations={appConfig?.translations}`, which is undefined for every
40
+ // project that has not deployed custom code. A nested provider that built its
41
+ // own isolated instance would shadow the host's and render the raw key.
42
+ render(
43
+ <FireCMSi18nProvider translations={HOST_TRANSLATIONS}>
44
+ <FireCMSi18nProvider translations={undefined}>
45
+ <Str k={"host_only_key"}/>
46
+ </FireCMSi18nProvider>
47
+ </FireCMSi18nProvider>
48
+ );
49
+ expect(screen.getByTestId("host_only_key").textContent).toBe("Explore your Firestore data");
50
+ expect(screen.getByTestId("host_only_key").textContent).not.toBe("host_only_key");
51
+ });
52
+
53
+ it("does not let a nested provider clobber a host override", () => {
54
+ render(
55
+ <FireCMSi18nProvider translations={HOST_TRANSLATIONS}>
56
+ <FireCMSi18nProvider translations={undefined}>
57
+ <Str k={"save"}/>
58
+ </FireCMSi18nProvider>
59
+ </FireCMSi18nProvider>
60
+ );
61
+ expect(screen.getByTestId("save").textContent).toBe("Publish");
62
+ });
63
+
64
+ it("lets a nested provider add its own translations on top of the host's", () => {
65
+ render(
66
+ <FireCMSi18nProvider translations={HOST_TRANSLATIONS}>
67
+ <FireCMSi18nProvider translations={{ en: { nested_key: "From the nested provider" } } as any}>
68
+ <>
69
+ <Str k={"host_only_key"}/>
70
+ <Str k={"nested_key"}/>
71
+ </>
72
+ </FireCMSi18nProvider>
73
+ </FireCMSi18nProvider>
74
+ );
75
+ expect(screen.getByTestId("host_only_key").textContent).toBe("Explore your Firestore data");
76
+ expect(screen.getByTestId("nested_key").textContent).toBe("From the nested provider");
77
+ });
78
+
79
+ it("still serves the built-in core strings through a nested provider", () => {
80
+ render(
81
+ <FireCMSi18nProvider>
82
+ <FireCMSi18nProvider translations={undefined}>
83
+ <Str k={"discard"}/>
84
+ </FireCMSi18nProvider>
85
+ </FireCMSi18nProvider>
86
+ );
87
+ const el = screen.getByTestId("discard");
88
+ expect(el.textContent).not.toBe("discard");
89
+ expect(el.textContent?.length).toBeGreaterThan(0);
90
+ });
91
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @jest-environment jsdom
3
+ */
4
+ import React from "react";
5
+ import { render, screen } from "@testing-library/react";
6
+ import { useTranslation } from "react-i18next";
7
+ import { FireCMSi18nProvider } from "../FireCMSi18nProvider";
8
+
9
+ function Str({ k }: { k: string }) {
10
+ const { t } = useTranslation("firecms_core");
11
+ return <span data-testid={k}>{t(k)}</span>;
12
+ }
13
+
14
+ const host = { en: { host_key: "Host" } } as any;
15
+ const projectA = { en: { only_in_a: "From project A" } } as any;
16
+ const projectB = { en: { only_in_b: "From project B" } } as any;
17
+
18
+ describe("nested provider lifecycle", () => {
19
+ beforeEach(() => window.localStorage.clear());
20
+
21
+ it("does not carry one project's translations into the next", () => {
22
+ // The real shape of a project switch: App.tsx's provider stays mounted while
23
+ // FireCMSCloudApp's inner one is torn down and rebuilt with another project's
24
+ // translations. Both share the outer i18next instance now, so anything the
25
+ // first project added could outlive it.
26
+ const { rerender } = render(
27
+ <FireCMSi18nProvider translations={host}>
28
+ <FireCMSi18nProvider key={"a"} translations={projectA}>
29
+ <Str k={"only_in_a"}/>
30
+ </FireCMSi18nProvider>
31
+ </FireCMSi18nProvider>
32
+ );
33
+ expect(screen.getByTestId("only_in_a").textContent).toBe("From project A");
34
+
35
+ rerender(
36
+ <FireCMSi18nProvider translations={host}>
37
+ <FireCMSi18nProvider key={"b"} translations={projectB}>
38
+ <>
39
+ <Str k={"only_in_b"}/>
40
+ <Str k={"only_in_a"}/>
41
+ </>
42
+ </FireCMSi18nProvider>
43
+ </FireCMSi18nProvider>
44
+ );
45
+
46
+ expect(screen.getByTestId("only_in_b").textContent).toBe("From project B");
47
+ expect(screen.getByTestId("only_in_a").textContent).toBe("only_in_a");
48
+ });
49
+
50
+ it("keeps the host's own translations across the switch", () => {
51
+ const { rerender } = render(
52
+ <FireCMSi18nProvider translations={host}>
53
+ <FireCMSi18nProvider key={"a"} translations={projectA}>
54
+ <Str k={"host_key"}/>
55
+ </FireCMSi18nProvider>
56
+ </FireCMSi18nProvider>
57
+ );
58
+ rerender(
59
+ <FireCMSi18nProvider translations={host}>
60
+ <FireCMSi18nProvider key={"b"} translations={projectB}>
61
+ <Str k={"host_key"}/>
62
+ </FireCMSi18nProvider>
63
+ </FireCMSi18nProvider>
64
+ );
65
+ expect(screen.getByTestId("host_key").textContent).toBe("Host");
66
+ });
67
+ });
@@ -55,6 +55,7 @@ export function useBuildDataSource({
55
55
  */
56
56
  fetchCollection: useCallback(<M extends Record<string, any>>({
57
57
  path,
58
+ pathSegments,
58
59
  collection,
59
60
  filter,
60
61
  limit,
@@ -67,6 +68,7 @@ export function useBuildDataSource({
67
68
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
68
69
  return usedDelegate.fetchCollection<M>({
69
70
  path,
71
+ pathSegments,
70
72
  filter,
71
73
  limit,
72
74
  startAfter,
@@ -97,6 +99,7 @@ export function useBuildDataSource({
97
99
  ? useCallback(<M extends Record<string, any>>(
98
100
  {
99
101
  path,
102
+ pathSegments,
100
103
  collection: collectionProp,
101
104
  filter,
102
105
  limit,
@@ -109,7 +112,7 @@ export function useBuildDataSource({
109
112
  }: ListenCollectionProps<M>
110
113
  ): () => void => {
111
114
 
112
- const collection = collectionProp ?? navigationController.getCollection(path);
115
+ const collection = collectionProp ?? navigationController.getCollection(path, false, pathSegments);
113
116
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
114
117
 
115
118
  if (!usedDelegate.listenCollection)
@@ -117,6 +120,7 @@ export function useBuildDataSource({
117
120
 
118
121
  return usedDelegate.listenCollection<M>({
119
122
  path,
123
+ pathSegments,
120
124
  filter,
121
125
  limit,
122
126
  startAfter,
@@ -139,6 +143,7 @@ export function useBuildDataSource({
139
143
  */
140
144
  fetchEntity: useCallback(<M extends Record<string, any>>({
141
145
  path,
146
+ pathSegments,
142
147
  entityId,
143
148
  collection
144
149
  }: FetchEntityProps<M>
@@ -146,6 +151,7 @@ export function useBuildDataSource({
146
151
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
147
152
  return usedDelegate.fetchEntity({
148
153
  path,
154
+ pathSegments,
149
155
  entityId,
150
156
  collection
151
157
  });
@@ -165,6 +171,7 @@ export function useBuildDataSource({
165
171
  ? useCallback(<M extends Record<string, any>>(
166
172
  {
167
173
  path,
174
+ pathSegments,
168
175
  entityId,
169
176
  collection,
170
177
  onUpdate,
@@ -177,6 +184,7 @@ export function useBuildDataSource({
177
184
 
178
185
  return usedDelegate.listenEntity<M>({
179
186
  path,
187
+ pathSegments,
180
188
  entityId,
181
189
  onUpdate,
182
190
  onError,
@@ -198,13 +206,14 @@ export function useBuildDataSource({
198
206
  saveEntity: useCallback(async <M extends Record<string, any>>(
199
207
  {
200
208
  path,
209
+ pathSegments,
201
210
  entityId,
202
211
  values,
203
212
  collection: collectionProp,
204
213
  status
205
214
  }: SaveEntityProps<M>): Promise<Entity<M>> => {
206
215
 
207
- const collection = collectionProp ?? navigationController.getCollection(path);
216
+ const collection = collectionProp ?? navigationController.getCollection(path, false, pathSegments);
208
217
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
209
218
 
210
219
  const resolvedCollection = collection
@@ -244,6 +253,7 @@ export function useBuildDataSource({
244
253
  try {
245
254
  const entities = await usedDelegate.fetchCollection({
246
255
  path,
256
+ pathSegments,
247
257
  orderBy: orderProperty,
248
258
  order: "asc",
249
259
  limit: 1,
@@ -270,14 +280,21 @@ export function useBuildDataSource({
270
280
 
271
281
  return usedDelegate.saveEntity({
272
282
  path,
283
+ pathSegments,
273
284
  collection,
274
285
  entityId,
275
286
  values: finalValues,
276
287
  status
277
288
  }).then((res) => {
289
+ // Keep everything the delegate resolved — `databaseId`, and `pathSegments` in
290
+ // particular — so a just-saved entity is as resolvable as a fetched one.
291
+ // Only the values are re-mapped back to the CMS model.
278
292
  return {
279
- id: res.id,
280
- path: res.path,
293
+ ...res,
294
+ // A delegate that predates `pathSegments` returns none; the segments we
295
+ // just saved with describe exactly this entity's collection, so they are
296
+ // the authoritative answer rather than a guess.
297
+ pathSegments: res.pathSegments ?? pathSegments,
281
298
  values: usedDelegate.delegateToCMSModel(finalValues)
282
299
  } as Entity<M>;
283
300
  });
@@ -292,12 +309,15 @@ export function useBuildDataSource({
292
309
  deleteEntity: useCallback(<M extends Record<string, any>>(
293
310
  {
294
311
  entity,
312
+ pathSegments,
295
313
  collection
296
314
  }: DeleteEntityProps<M>
297
315
  ): Promise<void> => {
298
316
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
299
317
  return usedDelegate.deleteEntity({
300
318
  entity,
319
+ // Falls back to what the entity carries from the fetch that produced it.
320
+ pathSegments: pathSegments ?? entity.pathSegments,
301
321
  collection
302
322
  });
303
323
  }, [delegate.deleteEntity]),
@@ -317,33 +337,32 @@ export function useBuildDataSource({
317
337
  name: string,
318
338
  value: any,
319
339
  entityId?: string,
320
- collection?: EntityCollection
340
+ collection?: EntityCollection,
341
+ pathSegments?: string[]
321
342
  ): Promise<boolean> => {
322
343
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
323
- return usedDelegate.checkUniqueField(path, name, value, entityId, collection);
344
+ return usedDelegate.checkUniqueField(path, name, value, entityId, collection, pathSegments);
324
345
  }, [delegate.checkUniqueField]),
325
346
 
326
- generateEntityId: useCallback((path: string, collection: EntityCollection): string => {
347
+ generateEntityId: useCallback((path: string, collection: EntityCollection, pathSegments?: string[]): string => {
327
348
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
328
- return usedDelegate.generateEntityId(path, collection);
349
+ return usedDelegate.generateEntityId(path, collection, pathSegments);
329
350
  }, [delegate.generateEntityId]),
330
351
 
352
+ // Typed with the declared props rather than an inline duplicate: the duplicate is
353
+ // free to drift from the interface, and a field it forgets is dropped in silence.
331
354
  countEntities: delegate.countEntities ? async ({
332
355
  path,
356
+ pathSegments,
333
357
  collection,
334
358
  filter,
335
359
  order,
336
360
  orderBy
337
- }: {
338
- path: string,
339
- collection: EntityCollection<any>,
340
- filter?: FilterValues<Extract<keyof any, string>>,
341
- orderBy?: string,
342
- order?: "desc" | "asc",
343
- }): Promise<number> => {
361
+ }: FetchCollectionProps<any>): Promise<number> => {
344
362
  const usedDelegate = collection?.overrides?.dataSourceDelegate ?? delegate;
345
363
  return usedDelegate.countEntities!({
346
364
  path,
365
+ pathSegments,
347
366
  filter,
348
367
  orderBy,
349
368
  order,
@@ -353,11 +372,13 @@ export function useBuildDataSource({
353
372
 
354
373
  isFilterCombinationValid: useCallback(({
355
374
  path,
375
+ pathSegments,
356
376
  databaseId,
357
377
  filterValues,
358
378
  sortBy
359
379
  }: {
360
380
  path: string,
381
+ pathSegments?: string[],
361
382
  databaseId?: string,
362
383
  filterValues: FilterValues<any>,
363
384
  sortBy?: [string, "asc" | "desc"]
@@ -367,6 +388,7 @@ export function useBuildDataSource({
367
388
  return delegate.isFilterCombinationValid(
368
389
  {
369
390
  path,
391
+ pathSegments,
370
392
  databaseId,
371
393
  filterValues,
372
394
  sortBy
@@ -13,6 +13,7 @@ import {
13
13
  import { getNavigationEntriesFromPath, NavigationViewInternal } from "../util/navigation_from_path";
14
14
  import { useLocation } from "react-router-dom";
15
15
  import {
16
+ encodeEntityId,
16
17
  removeInitialAndTrailingSlashes,
17
18
  resolveCollection,
18
19
  resolveDefaultSelectedView,
@@ -223,12 +224,14 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
223
224
  let sidePanel: EntitySidePanelProps<any> | undefined = undefined;
224
225
  let lastCollectionPath = "";
225
226
  let lastCollectionId: string | undefined = undefined;
227
+ let lastCollectionSegments: string[] | undefined = undefined;
226
228
  for (let i = 0; i < navigationViewsForPath.length; i++) {
227
229
  const navigationEntry = navigationViewsForPath[i];
228
230
 
229
231
  if (navigationEntry.type === "collection") {
230
232
  lastCollectionPath = navigationEntry.path;
231
233
  lastCollectionId = navigationEntry.collection.id;
234
+ lastCollectionSegments = navigationEntry.pathSegments;
232
235
  }
233
236
 
234
237
  const previousEntry = navigationViewsForPath[i - 1];
@@ -236,6 +239,7 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
236
239
  sidePanel = {
237
240
  path: navigationEntry.path,
238
241
  fullIdPath: navigationEntry.fullIdPath,
242
+ pathSegments: navigationEntry.pathSegments,
239
243
  entityId: navigationEntry.entityId,
240
244
  copy: false,
241
245
  width: navigationEntry.parentCollection?.sideDialogWidth
@@ -258,6 +262,9 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
258
262
  sidePanel = {
259
263
  path: lastCollectionPath,
260
264
  fullIdPath: lastCollectionId,
265
+ // A new entity still needs unambiguous segments: it may be created inside a
266
+ // subcollection whose parent id contains "/".
267
+ pathSegments: lastCollectionSegments,
261
268
  copy: false
262
269
  }
263
270
  }
@@ -274,11 +281,17 @@ const propsToSidePanel = (props: EntitySidePanelProps,
274
281
  locationSearch: string
275
282
  ): SideDialogPanelProps => {
276
283
 
277
- const collectionPath = removeInitialAndTrailingSlashes(props.path);
284
+ // The URL is the ESCAPED representation of the chain — it is what `buildSidePanelsFromUrl`
285
+ // and `FireCMSRoute` parse back on reload, on a deep link, and on every pathname change.
286
+ // `props.path` is the RAW datasource path, so a parent entity id containing "/" reads back
287
+ // there as extra collection/entity hops and resolves to a different entity entirely.
288
+ // `fullIdPath` carries the same chain with its ids already escaped, which is what it is
289
+ // threaded alongside `path` for.
290
+ const urlChain = removeInitialAndTrailingSlashes(props.fullIdPath ?? props.path);
278
291
 
279
292
  const urlPath = props.entityId
280
- ? buildUrlCollectionPath(`${collectionPath}/${props.entityId}${props.selectedTab ? "/" + props.selectedTab : ""}${locationSearch}#${SIDE_URL_HASH}`)
281
- : buildUrlCollectionPath(`${collectionPath}${locationSearch}#${NEW_URL_HASH}`);
293
+ ? buildUrlCollectionPath(`${urlChain}/${encodeEntityId(props.entityId)}${props.selectedTab ? "/" + props.selectedTab : ""}${locationSearch}#${SIDE_URL_HASH}`)
294
+ : buildUrlCollectionPath(`${urlChain}${locationSearch}#${NEW_URL_HASH}`);
282
295
 
283
296
  const resolvedPanelProps: EntitySidePanelProps<any> = {
284
297
  ...props,
@@ -287,10 +300,12 @@ const propsToSidePanel = (props: EntitySidePanelProps,
287
300
 
288
301
  const entityViewWidth = getEntityViewWidth(props, smallLayout, customizationController, authController);
289
302
  return {
290
- key: `${props.path}/${props.entityId}`,
303
+ // Built from the escaped chain so that two different chains which happen to flatten
304
+ // to the same raw string are still two different panels.
305
+ key: `${urlChain}/${props.entityId ? encodeEntityId(props.entityId) : ""}`,
291
306
  component: undefined, // Lazy render in SideDialogs for better performance
292
307
  urlPath: urlPath,
293
- parentUrlPath: buildUrlCollectionPath(collectionPath),
308
+ parentUrlPath: buildUrlCollectionPath(urlChain),
294
309
  width: entityViewWidth,
295
310
  onClose: props.onClose,
296
311
  additional: resolvedPanelProps
package/src/locales/de.ts CHANGED
@@ -65,8 +65,19 @@ export const de: FireCMSTranslations = {
65
65
  select_from: "Aus {{name}} auswählen",
66
66
  done: "Fertig",
67
67
  log_out: "Abmelden",
68
- license_needed: "Lizenz erforderlich",
69
- license_description: "Sie benötigen eine gültige Lizenz, um FireCMS PRO zu verwenden. Bitte wenden Sie sich an {{email}} für weitere Informationen.",
68
+ license_trial_banner: "FireCMS PRO-Testphase: noch {{days}} Tage in Produktion.",
69
+ license_trial_banner_one: "FireCMS PRO-Testphase: noch {{days}} Tag in Produktion.",
70
+ license_expired_banner: "Ihre PRO-Testphase ist am {{date}} abgelaufen. PRO-Funktionen wie Schema-Editor, Import/Export und Verlauf sind pausiert; Ihre Daten und Sammlungen funktionieren weiterhin.",
71
+ license_expired_banner_undated: "Ihre PRO-Testphase ist abgelaufen. PRO-Funktionen wie Schema-Editor, Import/Export und Verlauf sind pausiert; Ihre Daten und Sammlungen funktionieren weiterhin.",
72
+ license_invalid_project_banner: "Dieser Lizenzschlüssel ist nicht mit dem Projekt {{projectId}} verknüpft. Bis dahin sind die PRO-Funktionen pausiert.",
73
+ license_invalid_project_banner_unnamed: "Dieser Lizenzschlüssel ist nicht mit diesem Projekt verknüpft. Bis dahin sind die PRO-Funktionen pausiert.",
74
+ license_over_quota_banner: "Diese Lizenz deckt {{licensed}} Projekte ab, verknüpft sind aber {{actual}}.",
75
+ license_over_quota_banner_one: "Diese Lizenz deckt {{licensed}} Projekt ab, verknüpft sind aber {{actual}}.",
76
+ license_get_license: "Lizenz erwerben",
77
+ license_update_license: "Lizenz aktualisieren",
78
+ license_add_project_to_license: "Zu Ihrer Lizenz hinzufügen",
79
+ pro_features_paused: "PRO-Funktionen pausiert",
80
+ user_management_paused_description: "Die Verwaltung von Benutzern und Rollen ist Teil von FireCMS PRO. Ihre bestehenden Benutzer und Rollen legen weiterhin fest, wer sich anmelden darf und was er tun kann. Bearbeiten können Sie sie wieder, sobald dieses Projekt eine Lizenz hat.",
70
81
  column_cannot_be_edited: "Diese Spalte kann nicht direkt bearbeitet werden",
71
82
  close: "Schließen",
72
83
  unsaved_local_changes: "Ungespeicherte lokale Änderungen",
@@ -146,8 +157,8 @@ export const de: FireCMSTranslations = {
146
157
  // ─── Error states ─────────────────────────────────────────────
147
158
  error: "Fehler",
148
159
  error_uploading_file: "Fehler beim Hochladen der Datei",
149
- error_deleting: "Fehler beim Löschen",
150
- error_before_delete: "Fehler vor dem Löschen",
160
+ error_deleting: "Fehler beim Löschen: {{message}}",
161
+ error_before_delete: "Fehler vor dem Löschen: {{message}}",
151
162
  error_updating_asset: "Fehler beim Aktualisieren des Assets",
152
163
  error_deleting_asset: "Fehler beim Löschen des Assets",
153
164
  error_firestore_index: "Für diese Abfrage ist ein Firestore-Index erforderlich.",
@@ -389,6 +400,7 @@ export const de: FireCMSTranslations = {
389
400
  download: "Herunterladen",
390
401
  large_number_of_documents: "Diese Sammlung hat eine große Anzahl von Dokumenten ({{count}}).",
391
402
  include_undefined_values: "Undefinierte Werte einschließen",
403
+ export_apply_filter_sort: "Nur Ergebnisse exportieren, die dem aktuellen Filter/der aktuellen Sortierung entsprechen",
392
404
  submit: "Einreichen",
393
405
 
394
406
  no_filterable_properties: "Keine filterbaren Eigenschaften verfügbar",
@@ -438,6 +450,7 @@ export const de: FireCMSTranslations = {
438
450
  roles_menu: "Rollen",
439
451
  project_settings: "Projekteinstellungen",
440
452
  firestore_manager: "Firestore-Manager",
453
+ datatalk: "DataTalk",
441
454
  manage_your_firestore_data: "Deine Firestore-Daten verwalten",
442
455
 
443
456
  // ─── FireCMS Cloud Login ──────────────────────────────────────
@@ -454,6 +467,7 @@ export const de: FireCMSTranslations = {
454
467
  not_the_admin_panel: "nicht auf das Admin-Panel.",
455
468
  join_our_newsletter: "Melden Sie sich für unseren Newsletter an. Kein Spam, nur wichtige Updates!",
456
469
  by_signing_in_you_agree_to_our: "Mit der Anmeldung stimmen Sie unseren",
470
+ please_accept_the_terms_to_continue: "Bitte akzeptieren Sie die Nutzungsbedingungen, um fortzufahren",
457
471
  terms_and_conditions: "Allgemeinen Geschäftsbedingungen",
458
472
  and_our: "und unserer",
459
473
  privacy_policy: "Datenschutzerklärung",
@@ -496,6 +510,7 @@ export const de: FireCMSTranslations = {
496
510
  licensed_projects_lowercase: "lizenzierte projekte",
497
511
  manage: "Verwalten",
498
512
  update_payment_method: "Zahlungsmethode aktualisieren",
513
+ stripe_portal_error: "Stripe konnte nicht geöffnet werden: {{message}}",
499
514
  your_firecms_cloud_projects: "Ihre FireCMS Cloud Projekte",
500
515
  status_active: "Aktiv",
501
516
  status_trialing: "Testphase",
@@ -586,6 +601,10 @@ export const de: FireCMSTranslations = {
586
601
  order_property_not_found: "Eigenschaft \"{{property}}\" existiert nicht oder ist keine Zahlen-Eigenschaft. Bitte wählen Sie eine gültige Eigenschaft oder löschen Sie die Auswahl.",
587
602
  no_number_properties: "Keine Zahlen-Eigenschaften gefunden. Fügen Sie eine Zahlen-Eigenschaft hinzu, um die Sortierung zu aktivieren.",
588
603
  order_property_description: "Wählen Sie eine Zahlen-Eigenschaft, um die Reihenfolge der Elemente beizubehalten",
604
+ image_property: "Bild-Eigenschaft",
605
+ image_property_not_found: "Eigenschaft \"{{property}}\" existiert nicht oder ist keine Bild-Eigenschaft. Bitte wählen Sie eine gültige Eigenschaft oder löschen Sie die Auswahl.",
606
+ no_image_properties: "Keine Bild-Eigenschaften gefunden. Fügen Sie eine Speicher- oder Bild-URL-Eigenschaft hinzu, um sie als Vorschaubild zu verwenden.",
607
+ image_property_description: "Wählen Sie die Eigenschaft, die als Vorschaubild in den Ansichten Karten und Kanban verwendet wird. Wenn nicht festgelegt, wird automatisch die erste passende Bild-Eigenschaft verwendet.",
589
608
  display_settings: "Anzeigeeinstellungen",
590
609
  default_row_size: "Standard-Zeilengröße",
591
610
  side_dialog_width: "Seitendialog-Breite",
package/src/locales/en.ts CHANGED
@@ -73,8 +73,19 @@ export const en: FireCMSTranslations = {
73
73
  select_from: "Select from {{name}}",
74
74
  done: "Done",
75
75
  log_out: "Log out",
76
- license_needed: "License needed",
77
- license_description: "You need a valid license to use FireCMS PRO. Please reach out at {{email}} for more information.",
76
+ license_trial_banner: "FireCMS PRO trial: {{days}} days left in production.",
77
+ license_trial_banner_one: "FireCMS PRO trial: {{days}} day left in production.",
78
+ license_expired_banner: "Your PRO trial ended on {{date}}. PRO features such as the schema editor, import/export and history are paused; your data and collections still work.",
79
+ license_expired_banner_undated: "Your PRO trial has ended. PRO features such as the schema editor, import/export and history are paused; your data and collections still work.",
80
+ license_invalid_project_banner: "This license key is not linked to project {{projectId}}. PRO features are paused until it is.",
81
+ license_invalid_project_banner_unnamed: "This license key is not linked to this project. PRO features are paused until it is.",
82
+ license_over_quota_banner: "This license covers {{licensed}} projects and {{actual}} are linked to it.",
83
+ license_over_quota_banner_one: "This license covers {{licensed}} project and {{actual}} are linked to it.",
84
+ license_get_license: "Get a license",
85
+ license_update_license: "Update your license",
86
+ license_add_project_to_license: "Add it to your license",
87
+ pro_features_paused: "PRO features paused",
88
+ user_management_paused_description: "Managing users and roles is part of FireCMS PRO. Your existing users and roles still decide who can sign in and what they can do, and you can edit them again once this project has a license.",
78
89
  column_cannot_be_edited: "This column can't be edited directly",
79
90
  close: "Close",
80
91
  unsaved_local_changes: "Unsaved Local changes",
@@ -154,8 +165,8 @@ export const en: FireCMSTranslations = {
154
165
  // ─── Error states ─────────────────────────────────────────────
155
166
  error: "Error",
156
167
  error_uploading_file: "Error uploading file",
157
- error_deleting: "Error deleting",
158
- error_before_delete: "Error before delete",
168
+ error_deleting: "Error deleting: {{message}}",
169
+ error_before_delete: "Error before delete: {{message}}",
159
170
  error_updating_asset: "Error updating asset",
160
171
  error_deleting_asset: "Error deleting asset",
161
172
  error_firestore_index: "A Firestore index is required for this query.",
@@ -397,6 +408,7 @@ export const en: FireCMSTranslations = {
397
408
  download: "Download",
398
409
  large_number_of_documents: "This collections has a large number of documents ({{count}}).",
399
410
  include_undefined_values: "Include undefined values",
411
+ export_apply_filter_sort: "Only export results matching the current filter/sort",
400
412
  submit: "Submit",
401
413
 
402
414
  no_filterable_properties: "No filterable properties available",
@@ -447,6 +459,9 @@ export const en: FireCMSTranslations = {
447
459
  project_settings: "Project settings",
448
460
  firestore_manager: "Firestore Manager",
449
461
  manage_your_firestore_data: "Manage your Firestore data",
462
+ // A product name. It is a key rather than a literal so a locale that uses a
463
+ // different script can transliterate it; most will inherit this value.
464
+ datatalk: "DataTalk",
450
465
 
451
466
  // ─── FireCMS Cloud Login ──────────────────────────────────────
452
467
  build_admin_panel_in_minutes: "Build Your Firebase Admin Panel in Minutes",
@@ -462,6 +477,7 @@ export const en: FireCMSTranslations = {
462
477
  not_the_admin_panel: "not the admin panel.",
463
478
  join_our_newsletter: "Join our newsletter. No spam, only important updates!",
464
479
  by_signing_in_you_agree_to_our: "By signing in you agree to our",
480
+ please_accept_the_terms_to_continue: "Please accept the terms and conditions to continue",
465
481
  terms_and_conditions: "Terms and Conditions",
466
482
  and_our: "and our",
467
483
  privacy_policy: "Privacy policy",
@@ -504,6 +520,7 @@ export const en: FireCMSTranslations = {
504
520
  licensed_projects_lowercase: "licensed projects",
505
521
  manage: "Manage",
506
522
  update_payment_method: "Update payment method",
523
+ stripe_portal_error: "Could not open Stripe: {{message}}",
507
524
  your_firecms_cloud_projects: "Your FireCMS Cloud projects",
508
525
  status_active: "Active",
509
526
  status_trialing: "Trialing",
@@ -598,6 +615,10 @@ export const en: FireCMSTranslations = {
598
615
  order_property_not_found: "Property \"{{property}}\" does not exist or is not a number property. Please select a valid property or clear the selection.",
599
616
  no_number_properties: "No number properties found. Add a number property to enable ordering.",
600
617
  order_property_description: "Select a number property to persist the order of items",
618
+ image_property: "Image Property",
619
+ image_property_not_found: "Property \"{{property}}\" does not exist or is not an image property. Please select a valid property or clear the selection.",
620
+ no_image_properties: "No image properties found. Add a storage or URL image property to use as a thumbnail.",
621
+ image_property_description: "Select the property used as the thumbnail image in Cards and Kanban view modes. If not set, the first matching image property will be used automatically.",
601
622
  display_settings: "Display settings",
602
623
  default_row_size: "Default row size",
603
624
  side_dialog_width: "Side dialog width",