@firecms/core 3.3.0-canary.d3242eb → 3.4.0-canary.00f9be5

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 (221) 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-BF5vcQPY.js +703 -0
  31. package/dist/de-BF5vcQPY.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-DH9_wyFI.js +703 -0
  42. package/dist/fr-DH9_wyFI.js.map +1 -0
  43. package/dist/hi-BM8dm1Lu.js +703 -0
  44. package/dist/hi-BM8dm1Lu.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 +4429 -10479
  56. package/dist/index.es.js.map +1 -1
  57. package/dist/index.umd.js +9313 -6582
  58. package/dist/index.umd.js.map +1 -1
  59. package/dist/it-D3K1gfe4.js +703 -0
  60. package/dist/it-D3K1gfe4.js.map +1 -0
  61. package/dist/locales/pl.d.ts +2 -0
  62. package/dist/pl-CP1zeLzn.js +705 -0
  63. package/dist/pl-CP1zeLzn.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-Cus7eKQR.js +705 -0
  67. package/dist/pt-Cus7eKQR.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 +55 -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 +54 -5
  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 -11
  100. package/src/app/Scaffold.tsx +54 -45
  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 +71 -17
  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/HomePage/NavigationCardBinding.tsx +6 -3
  118. package/src/components/LicenseBanner.tsx +207 -0
  119. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
  120. package/src/components/ReferenceWidget.tsx +1 -1
  121. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
  122. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
  123. package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
  124. package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
  125. package/src/components/VirtualTable/VirtualTable.tsx +19 -8
  126. package/src/components/common/default_entity_actions.tsx +22 -2
  127. package/src/components/common/table_url_params.ts +172 -0
  128. package/src/components/common/useDataSourceTableController.tsx +126 -163
  129. package/src/components/common/useTableSearchHelper.ts +26 -2
  130. package/src/components/index.tsx +1 -0
  131. package/src/contexts/LicenseStatusContext.tsx +50 -0
  132. package/src/contexts/SnackbarProvider.tsx +14 -1
  133. package/src/core/DefaultAppBar.tsx +28 -16
  134. package/src/core/DefaultDrawer.tsx +151 -65
  135. package/src/core/DrawerNavigationGroup.tsx +27 -29
  136. package/src/core/DrawerNavigationItem.tsx +10 -12
  137. package/src/core/EntityEditView.tsx +77 -35
  138. package/src/core/EntityEditViewFormActions.tsx +3 -2
  139. package/src/core/EntitySidePanel.tsx +8 -4
  140. package/src/core/FireCMS.tsx +73 -47
  141. package/src/core/SideDialogs.tsx +44 -1
  142. package/src/core/field_configs.tsx +15 -0
  143. package/src/core/index.tsx +1 -0
  144. package/src/core/plugin_translations.ts +87 -0
  145. package/src/core/pro_plugins.ts +79 -0
  146. package/src/editor/components/SlashCommandMenu.tsx +4 -1
  147. package/src/editor/extensions/Image.ts +17 -1
  148. package/src/editor/hooks/useProseMirror.ts +5 -0
  149. package/src/editor/index.ts +11 -1
  150. package/src/editor/lazy_editor.tsx +33 -0
  151. package/src/form/EntityForm.tsx +31 -10
  152. package/src/form/EntityFormActions.tsx +2 -0
  153. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
  154. package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
  155. package/src/form/index.tsx +1 -0
  156. package/src/hooks/data/delete.ts +8 -0
  157. package/src/hooks/data/save.ts +6 -1
  158. package/src/hooks/data/useCollectionFetch.tsx +21 -1
  159. package/src/hooks/data/useEntityFetch.tsx +35 -4
  160. package/src/hooks/index.tsx +2 -0
  161. package/src/hooks/useBuildNavigationController.tsx +75 -16
  162. package/src/hooks/useFireCMSContext.tsx +7 -3
  163. package/src/hooks/useLicenseStatus.tsx +32 -0
  164. package/src/hooks/useProjectLog.tsx +27 -9
  165. package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
  166. package/src/hooks/useTranslation.ts +3 -1
  167. package/src/hooks/useValidateAuthenticator.tsx +4 -0
  168. package/src/i18n/FireCMSi18nProvider.tsx +199 -23
  169. package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
  170. package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
  171. package/src/internal/useBuildDataSource.ts +37 -15
  172. package/src/internal/useBuildSideEntityController.tsx +20 -5
  173. package/src/locales/de.ts +37 -7
  174. package/src/locales/en.ts +41 -9
  175. package/src/locales/es.ts +37 -7
  176. package/src/locales/fr.ts +37 -7
  177. package/src/locales/hi.ts +37 -7
  178. package/src/locales/it.ts +37 -7
  179. package/src/locales/pl.ts +752 -0
  180. package/src/locales/pt.ts +37 -7
  181. package/src/preview/PropertyPreview.tsx +12 -0
  182. package/src/preview/components/DatePreview.tsx +2 -4
  183. package/src/preview/components/ReferencePreview.tsx +7 -3
  184. package/src/preview/components/StorageThumbnail.tsx +24 -2
  185. package/src/preview/index.ts +1 -0
  186. package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
  187. package/src/routes/FireCMSRoute.tsx +54 -27
  188. package/src/types/analytics.ts +3 -0
  189. package/src/types/collections.ts +34 -0
  190. package/src/types/datasource.ts +67 -4
  191. package/src/types/entities.ts +24 -1
  192. package/src/types/entity_actions.tsx +6 -0
  193. package/src/types/entity_callbacks.ts +24 -0
  194. package/src/types/firecms.tsx +19 -1
  195. package/src/types/firecms_context.tsx +9 -0
  196. package/src/types/index.ts +1 -0
  197. package/src/types/license.ts +63 -0
  198. package/src/types/navigation.ts +35 -5
  199. package/src/types/plugins.tsx +23 -1
  200. package/src/types/properties.ts +17 -0
  201. package/src/types/side_entity_controller.tsx +7 -0
  202. package/src/types/translations.ts +54 -5
  203. package/src/util/__tests__/collections.test.ts +32 -0
  204. package/src/util/__tests__/urls.test.ts +131 -0
  205. package/src/util/collections.ts +1 -1
  206. package/src/util/date_fns_locales.ts +168 -0
  207. package/src/util/entities.ts +16 -1
  208. package/src/util/entity_cache.ts +18 -0
  209. package/src/util/geopoint.ts +81 -0
  210. package/src/util/index.ts +3 -0
  211. package/src/util/lazy_eager.tsx +30 -8
  212. package/src/util/navigation_blocking.ts +45 -0
  213. package/src/util/navigation_from_path.ts +47 -7
  214. package/src/util/navigation_utils.ts +250 -5
  215. package/src/util/parent_references_from_path.ts +45 -4
  216. package/src/util/paths.ts +40 -3
  217. package/src/util/permissions.ts +22 -10
  218. package/src/util/previews.ts +8 -1
  219. package/src/util/resolutions.ts +11 -0
  220. package/src/util/urls.ts +52 -0
  221. package/src/util/useStorageUploadController.tsx +6 -1
@@ -1,6 +1,8 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import equal from "react-fast-compare"
3
3
  import { useBlocker, useNavigate } from "react-router-dom";
4
+ import { filterPausedPlugins, isProPaused } from "../core/pro_plugins";
5
+ import { useLicenseStatus } from "./useLicenseStatus";
4
6
 
5
7
  import {
6
8
  AuthController,
@@ -22,11 +24,14 @@ import {
22
24
  } from "../types";
23
25
  import {
24
26
  applyPermissionsFunctionIfEmpty,
27
+ collectionSegmentsFrom,
28
+ fullPathToCollectionSegments,
25
29
  getCollectionByPathOrId,
26
30
  mergeDeep,
27
31
  removeFunctions,
28
32
  removeInitialAndTrailingSlashes,
29
33
  resolveCollectionPathIds,
34
+ resolveCollectionPathSegments,
30
35
  resolvePermissions
31
36
  } from "../util";
32
37
  import { getParentReferencesFromPath } from "../util/parent_references_from_path";
@@ -112,7 +117,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
112
117
  views: viewsProp,
113
118
  adminViews: adminViewsProp,
114
119
  viewsOrder,
115
- plugins,
120
+ plugins: allPlugins,
116
121
  userConfigPersistence,
117
122
  dataSourceDelegate,
118
123
  disabled,
@@ -121,6 +126,27 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
121
126
 
122
127
  const navigate = useNavigate();
123
128
 
129
+ // While PRO is paused, PRO plugins contribute no collections, collection
130
+ // modifications, views or navigation entries. This hook runs above
131
+ // `FireCMS`, so the status arrives through the store `FireCMS` publishes to,
132
+ // and its arrival re-renders this hook, which resolves navigation again.
133
+ const licenseStatus = useLicenseStatus();
134
+ const proPaused = isProPaused(licenseStatus);
135
+ const plugins = useMemo(
136
+ () => filterPausedPlugins(allPlugins, licenseStatus),
137
+ [allPlugins, licenseStatus]
138
+ );
139
+
140
+ // Navigation resolves asynchronously, so a resolution started before the
141
+ // pause changed can finish after one started since. This ref holds the
142
+ // current pause so the older one is dropped instead of restoring what the
143
+ // paused plugins contributed. Declared before the effect that refreshes.
144
+ const proPausedRef = useRef(proPaused);
145
+ useEffect(() => {
146
+ proPausedRef.current = proPaused;
147
+ }, [proPaused]);
148
+ const resolvedProPausedRef = useRef<boolean | undefined>(undefined);
149
+
124
150
  const collectionsRef = useRef<EntityCollection[] | undefined>(undefined);
125
151
  const viewsRef = useRef<CMSView[] | undefined>(undefined);
126
152
  const adminViewsRef = useRef<CMSView[] | undefined>(undefined);
@@ -299,7 +325,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
299
325
  groups: uniqueGroups,
300
326
  onNavigationEntriesUpdate: onNavigationEntriesUpdateCallback!,
301
327
  };
302
- }, [navigationGroupMappings, buildCMSUrlPath, buildUrlCollectionPath, pluginGroups]);
328
+ }, [navigationGroupMappings, buildCMSUrlPath, buildUrlCollectionPath, pluginGroups, plugins]);
303
329
 
304
330
  const onNavigationEntriesOrderUpdate = useCallback((entries: NavigationGroupMapping[]) => {
305
331
  if (!plugins) {
@@ -339,6 +365,8 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
339
365
 
340
366
  console.debug("Refreshing navigation");
341
367
 
368
+ const resolvingWhilePaused = proPaused;
369
+
342
370
  try {
343
371
 
344
372
  const [resolvedCollections = [], resolvedViews, resolvedAdminViews = []] = await Promise.all([
@@ -348,10 +376,21 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
348
376
  ]
349
377
  );
350
378
 
379
+ // Superseded: a newer resolution runs with the current pause.
380
+ if (resolvingWhilePaused !== proPausedRef.current)
381
+ return;
382
+
351
383
  const computedTopLevelNav = computeTopNavigation(resolvedCollections, resolvedViews, resolvedAdminViews, viewsOrder, undefined, onNavigationEntriesOrderUpdate);
352
384
 
353
- let shouldUpdateTopLevelNav = false;
354
- if (!areCollectionListsEqual(collectionsRef.current ?? [], resolvedCollections)) {
385
+ // When the pause changes, everything below is replaced, including
386
+ // what the comparisons ignore: they skip functions, and a plugin may
387
+ // have added only collection callbacks, or only regrouped entries.
388
+ const pauseChanged = resolvedProPausedRef.current !== undefined
389
+ && resolvedProPausedRef.current !== resolvingWhilePaused;
390
+ resolvedProPausedRef.current = resolvingWhilePaused;
391
+
392
+ let shouldUpdateTopLevelNav = pauseChanged;
393
+ if (pauseChanged || !areCollectionListsEqual(collectionsRef.current ?? [], resolvedCollections)) {
355
394
  collectionsRef.current = resolvedCollections;
356
395
  console.debug("Collections have changed", resolvedCollections);
357
396
  shouldUpdateTopLevelNav = true;
@@ -360,7 +399,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
360
399
  collectionsRef.current = resolvedCollections;
361
400
  shouldUpdateTopLevelNav = true;
362
401
  }
363
- if (!equal(viewsRef.current, resolvedViews)) {
402
+ if (pauseChanged || !equal(viewsRef.current, resolvedViews)) {
364
403
  viewsRef.current = resolvedViews;
365
404
  shouldUpdateTopLevelNav = true;
366
405
  }
@@ -397,6 +436,8 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
397
436
  viewsProp,
398
437
  adminViewsProp,
399
438
  computeTopNavigation,
439
+ plugins,
440
+ proPaused
400
441
  ]);
401
442
 
402
443
  useEffect(() => {
@@ -405,16 +446,21 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
405
446
 
406
447
  const getCollection = useCallback((
407
448
  idOrPath: string,
408
- includeUserOverride = false
449
+ includeUserOverride = false,
450
+ pathSegments?: string[]
409
451
  ): EC | undefined => {
410
452
  const collections = collectionsRef.current;
411
453
  if (!collections)
412
454
  return undefined;
413
455
 
414
- const baseCollection = getCollectionByPathOrId(removeInitialAndTrailingSlashes(idOrPath), collections);
456
+ const baseCollection = getCollectionByPathOrId(removeInitialAndTrailingSlashes(idOrPath), collections, pathSegments);
415
457
 
416
458
  const userOverride = includeUserOverride ? userConfigPersistence?.getCollectionConfig(idOrPath) : undefined;
417
- const overriddenCollection = baseCollection ? mergeDeep(baseCollection, userOverride ?? {}) : undefined;
459
+ let overriddenCollection = baseCollection;
460
+ if (baseCollection && userOverride) {
461
+ const { properties, ...rest } = userOverride;
462
+ overriddenCollection = mergeDeep(baseCollection, rest);
463
+ }
418
464
 
419
465
  let result: Partial<EntityCollection> | undefined = overriddenCollection;
420
466
 
@@ -501,23 +547,33 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
501
547
  throw Error("Expected path starting with " + fullCollectionPath);
502
548
  }, [fullCollectionPath]);
503
549
 
504
- const resolveIdsFrom = useCallback((path: string): string => {
550
+ const resolveIdsFrom = useCallback((path: string, pathSegments?: string[]): string => {
551
+ const collections = collectionsRef.current ?? [];
552
+ return resolveCollectionPathIds(path, collections, pathSegments);
553
+ }, []);
554
+
555
+ const resolveSegmentsFrom = useCallback((pathSegments: string[]): string[] => {
505
556
  const collections = collectionsRef.current ?? [];
506
- return resolveCollectionPathIds(path, collections);
557
+ return resolveCollectionPathSegments(pathSegments, collections);
507
558
  }, []);
508
559
 
509
- const getAllParentReferencesForPath = useCallback((path: string): EntityReference[] => {
560
+ const getAllParentReferencesForPath = useCallback((path: string, pathSegments?: string[]): EntityReference[] => {
510
561
  const collections = collectionsRef.current ?? [];
511
562
  return getParentReferencesFromPath({
512
563
  path,
564
+ pathSegments,
513
565
  collections
514
566
  });
515
567
  }, []);
516
568
 
517
- const getParentCollectionIds = useCallback((path: string): string[] => {
569
+ const getParentCollectionIds = useCallback((path: string, pathSegments?: string[]): string[] => {
518
570
 
519
- const strings = path.split("/");
520
- const oddPathSegments = strings.filter((_, i) => i % 2 === 0);
571
+ // Positional filtering only picks out the collection segments while no entity id
572
+ // contains "/"; with one, every following segment shifts. Given the real boundaries
573
+ // the same rule applies correctly; without them this is the historical reading.
574
+ const oddPathSegments = pathSegments
575
+ ? collectionSegmentsFrom(pathSegments)
576
+ : fullPathToCollectionSegments(path);
521
577
  oddPathSegments.pop();
522
578
 
523
579
  const result: string[][] = [];
@@ -563,13 +619,16 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
563
619
  urlPathToDataPath,
564
620
  buildUrlCollectionPath,
565
621
  resolveIdsFrom,
622
+ resolveSegmentsFrom,
566
623
  topLevelNavigation,
567
624
  refreshNavigation,
568
625
  getParentReferencesFromPath: getAllParentReferencesForPath,
569
626
  getParentCollectionIds,
570
627
  convertIdsToPaths,
571
628
  navigate,
572
- plugins
629
+ // All of them, paused or not: `FireCMS` sends every key to the license
630
+ // check and applies the same pause to what it renders.
631
+ plugins: allPlugins
573
632
  };
574
633
  }
575
634
 
@@ -12,6 +12,7 @@ import { useCustomizationController } from "./useCustomizationController";
12
12
  import { useAnalyticsController } from "./useAnalyticsController";
13
13
  import React, { useEffect } from "react";
14
14
  import { useInternalUserManagementController } from "./useInternalUserManagementController";
15
+ import { useLicenseStatus } from "./useLicenseStatus";
15
16
 
16
17
  /**
17
18
  * Hook to retrieve the {@link FireCMSContext}.
@@ -36,6 +37,7 @@ export const useFireCMSContext = <USER extends User = User, AuthControllerType e
36
37
  const customizationController = useCustomizationController();
37
38
  const analyticsController = useAnalyticsController();
38
39
  const userManagement = useInternalUserManagementController<USER>();
40
+ const licenseStatus = useLicenseStatus();
39
41
 
40
42
  const fireCMSContextRef = React.useRef<FireCMSContext<USER, AuthControllerType>>({
41
43
  authController,
@@ -49,7 +51,8 @@ export const useFireCMSContext = <USER extends User = User, AuthControllerType e
49
51
  dialogsController,
50
52
  customizationController,
51
53
  analyticsController,
52
- userManagement
54
+ userManagement,
55
+ licenseStatus
53
56
  });
54
57
 
55
58
  useEffect(() => {
@@ -65,9 +68,10 @@ export const useFireCMSContext = <USER extends User = User, AuthControllerType e
65
68
  dialogsController,
66
69
  customizationController,
67
70
  analyticsController,
68
- userManagement
71
+ userManagement,
72
+ licenseStatus
69
73
  };
70
- }, [authController, dialogsController, navigation, sideDialogsController]);
74
+ }, [authController, dialogsController, navigation, sideDialogsController, licenseStatus]);
71
75
 
72
76
  return fireCMSContextRef.current;
73
77
  }
@@ -0,0 +1,32 @@
1
+ import { useContext, useSyncExternalStore } from "react";
2
+ import { AccessResponse } from "../types";
3
+ import {
4
+ getLicenseStatusSnapshot,
5
+ getServerLicenseStatusSnapshot,
6
+ LicenseStatusContext,
7
+ subscribeLicenseStatus
8
+ } from "../contexts/LicenseStatusContext";
9
+
10
+ /**
11
+ * The FireCMS license status of this project: whether PRO runs on a trial, is
12
+ * licensed, or is paused, plus the link to get a license.
13
+ *
14
+ * `null` until the license check answers, and when it is not sent at all
15
+ * (`telemetry={false}` without an `apiKey`). A response from an older server
16
+ * may lack `licenseState`; treat that as unknown. PRO features are paused when
17
+ * `blocked` is true; use `isProPaused(status)` for that check.
18
+ *
19
+ * Works inside `FireCMS` and also above it, e.g. in the component that calls
20
+ * `useBuildNavigationController`.
21
+ *
22
+ * @group Hooks and utilities
23
+ */
24
+ export function useLicenseStatus(): AccessResponse | null {
25
+ const fromContext = useContext(LicenseStatusContext);
26
+ const published = useSyncExternalStore(
27
+ subscribeLicenseStatus,
28
+ getLicenseStatusSnapshot,
29
+ getServerLicenseStatusSnapshot
30
+ );
31
+ return fromContext !== undefined ? fromContext : published;
32
+ }
@@ -1,14 +1,10 @@
1
1
  import { useEffect, useRef, useState } from "react";
2
- import { AuthController, DataSourceDelegate, FireCMSPlugin } from "../types";
2
+ import { AccessResponse, AuthController, DataSourceDelegate, FireCMSPlugin } from "../types";
3
+ import { PRO_PLUGIN_KEYS } from "../core/pro_plugins";
3
4
 
4
5
  export const DEFAULT_SERVER_DEV = "https://api-kdoe6pj3qq-ey.a.run.app";
5
6
  export const DEFAULT_SERVER = "https://api.firecms.co";
6
7
 
7
- export type AccessResponse = {
8
- blocked?: boolean;
9
- message?: string;
10
- }
11
-
12
8
  async function makeRequest(authController: AuthController, dataSourceKey: string, pluginKeys: string[] | undefined, apiKey?: string): Promise<AccessResponse> {
13
9
  let idToken: string | null;
14
10
  try {
@@ -40,24 +36,46 @@ export interface UseProjectLogParams {
40
36
  apiKey?: string;
41
37
  authController: AuthController;
42
38
  dataSourceDelegate: DataSourceDelegate;
39
+ /**
40
+ * Every plugin the app mounts, before any are paused: the server decides
41
+ * from these keys whether the project needs a license.
42
+ */
43
43
  plugins?: FireCMSPlugin<any, any, any>[];
44
+ /**
45
+ * When false, no request is sent, unless the request is a license check:
46
+ * with an `apiKey`, or with a PRO plugin mounted, it is always sent.
47
+ */
48
+ telemetry?: boolean;
44
49
  }
45
50
 
46
51
  export function useProjectLog({
47
52
  authController,
48
53
  dataSourceDelegate,
49
54
  plugins,
50
- apiKey
55
+ apiKey,
56
+ telemetry = true
51
57
  }: UseProjectLogParams): AccessResponse | null {
52
58
  const [accessResponse, setAccessResponse] = useState<AccessResponse | null>(null);
53
59
  const accessedUserRef = useRef<string | null>(null);
54
60
  const dataSourceKey = dataSourceDelegate.key;
55
61
  const pluginKeys = plugins?.map(plugin => plugin.key);
62
+ // Opting out is for apps without PRO plugins. With one mounted, the
63
+ // request is what starts the trial and pauses PRO when it ends, so
64
+ // skipping it would unlock PRO for good.
65
+ const runsProPlugin = (pluginKeys ?? []).some(key => PRO_PLUGIN_KEYS.includes(key));
66
+ const enabled = Boolean(apiKey) || runsProPlugin || telemetry !== false;
56
67
  useEffect(() => {
68
+ if (!enabled) return;
57
69
  if (authController.user && authController.user.uid !== accessedUserRef.current && !authController.initialLoading) {
58
- makeRequest(authController, dataSourceKey, pluginKeys, apiKey).then(setAccessResponse);
70
+ makeRequest(authController, dataSourceKey, pluginKeys, apiKey)
71
+ .then((response) => setAccessResponse(response && typeof response === "object" ? response : null))
72
+ .catch((error) => {
73
+ // The check never gates the CMS, so a failed one only
74
+ // leaves the license status unknown.
75
+ console.debug("FireCMS license check failed", error);
76
+ });
59
77
  accessedUserRef.current = authController.user.uid;
60
78
  }
61
- }, [authController, dataSourceKey, pluginKeys]);
79
+ }, [authController, dataSourceKey, pluginKeys, enabled]);
62
80
  return accessResponse;
63
81
  }
@@ -80,12 +80,14 @@ export function resolveNavigationFrom<M extends Record<string, any>, USER extend
80
80
  if (entry.type === "collection") {
81
81
  return Promise.resolve(entry);
82
82
  } else if (entry.type === "entity") {
83
- const collection = navigation.getCollection(entry.path);
83
+ const collection = navigation.getCollection(entry.path, false, entry.pathSegments);
84
84
  if (!collection) {
85
85
  throw Error(`No collection defined in the navigation for the entity with path ${entry.path}`);
86
86
  }
87
87
  return dataSource.fetchEntity({
88
88
  path: entry.path,
89
+ // The navigation entry already knows the real segment boundaries.
90
+ pathSegments: entry.pathSegments,
89
91
  entityId: entry.entityId,
90
92
  collection
91
93
  })
@@ -23,8 +23,10 @@ export function useTranslation() {
23
23
  * Also supports i18next interpolation variables, e.g.
24
24
  * t("add_to_field", { fieldName: "Tags" })
25
25
  * t("error_deleting", { message: err.message })
26
+ * Pass `count` as a number to pick a plural form (`key_one`, falling back
27
+ * to `key`); i18next ignores a string `count`.
26
28
  */
27
- const typedT = (key: string, vars?: Record<string, string>): string =>
29
+ const typedT = (key: string, vars?: Record<string, string | number>): string =>
28
30
  t(key, vars) as string;
29
31
 
30
32
  return { t: typedT, i18n };
@@ -74,6 +74,10 @@ export function useValidateAuthenticator<USER extends User = any>
74
74
 
75
75
  if (authenticator instanceof Function && delegateUser && !equal(checkedUserRef.current?.uid, delegateUser.uid)) {
76
76
  setAuthLoading(true);
77
+ // Reset the error of any previous authentication attempt. Otherwise, a
78
+ // rejected user would keep `canAccessMainView` to false for every user
79
+ // logging in afterwards, locking them out of the CMS until a page reload.
80
+ setNotAllowedError(false);
77
81
  try {
78
82
  const allowed = await authenticator({
79
83
  user: delegateUser,