@firecms/core 3.3.0 → 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.
- package/README.md +4 -0
- package/dist/app/useApp.d.ts +1 -0
- package/dist/components/EntityCollectionTable/column_utils.d.ts +4 -3
- package/dist/components/EntityCollectionTable/internal/CollectionTableToolbar.d.ts +6 -1
- package/dist/components/EntityCollectionTable/internal/popup_field/PopupFormField.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionBoardView.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionView.d.ts +8 -0
- package/dist/components/EntityCollectionView/EntityCollectionViewActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/EntityCollectionViewStartActions.d.ts +3 -1
- package/dist/components/EntityCollectionView/FiltersDialog.d.ts +2 -1
- package/dist/components/EntityCollectionView/useBoardDataController.d.ts +3 -1
- package/dist/components/EntityPreview.d.ts +3 -1
- package/dist/components/ErrorBoundary.d.ts +1 -3
- package/dist/components/LicenseBanner.d.ts +23 -0
- package/dist/components/ReferenceTable/ReferenceSelectionTable.d.ts +10 -1
- package/dist/components/SelectableTable/filters/filter_text_input.d.ts +30 -0
- package/dist/components/SelectableTable/filters/filter_text_input.test.d.ts +1 -0
- package/dist/components/common/table_url_params.d.ts +24 -0
- package/dist/components/common/useDataSourceTableController.d.ts +9 -1
- package/dist/components/common/useTableSearchHelper.d.ts +9 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/contexts/LicenseStatusContext.d.ts +16 -0
- package/dist/core/DefaultDrawer.d.ts +10 -3
- package/dist/core/EntityEditView.d.ts +9 -1
- package/dist/core/EntityEditViewFormActions.d.ts +1 -1
- package/dist/core/field_configs.d.ts +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/plugin_translations.d.ts +38 -0
- package/dist/core/pro_plugins.d.ts +40 -0
- package/dist/de-BF5vcQPY.js +703 -0
- package/dist/de-BF5vcQPY.js.map +1 -0
- package/dist/editor/extensions/Image.d.ts +11 -0
- package/dist/editor/index.d.ts +2 -1
- package/dist/editor/lazy_editor.d.ts +6 -0
- package/dist/editor-BSo_aS1I.js +4563 -0
- package/dist/editor-BSo_aS1I.js.map +1 -0
- package/dist/form/EntityForm.d.ts +8 -1
- package/dist/form/EntityFormActions.d.ts +2 -0
- package/dist/form/field_bindings/GeopointFieldBinding.d.ts +5 -0
- package/dist/form/index.d.ts +1 -0
- package/dist/fr-DH9_wyFI.js +703 -0
- package/dist/fr-DH9_wyFI.js.map +1 -0
- package/dist/hi-BM8dm1Lu.js +703 -0
- package/dist/hi-BM8dm1Lu.js.map +1 -0
- package/dist/hooks/data/delete.d.ts +1 -1
- package/dist/hooks/data/save.d.ts +1 -1
- package/dist/hooks/data/useCollectionFetch.d.ts +7 -1
- package/dist/hooks/data/useEntityFetch.d.ts +9 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/useLicenseStatus.d.ts +16 -0
- package/dist/hooks/useProjectLog.d.ts +11 -6
- package/dist/hooks/useTranslation.d.ts +1 -1
- package/dist/i18n/__tests__/FireCMSi18nProvider.test.d.ts +1 -0
- package/dist/i18n/__tests__/nested_provider_lifecycle.test.d.ts +1 -0
- package/dist/index.es.js +4516 -10668
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +9403 -6774
- package/dist/index.umd.js.map +1 -1
- package/dist/it-D3K1gfe4.js +703 -0
- package/dist/it-D3K1gfe4.js.map +1 -0
- package/dist/locales/pl.d.ts +2 -0
- package/dist/pl-CP1zeLzn.js +705 -0
- package/dist/pl-CP1zeLzn.js.map +1 -0
- package/dist/preview/index.d.ts +1 -0
- package/dist/preview/property_previews/GeopointPropertyPreview.d.ts +4 -0
- package/dist/pt-Cus7eKQR.js +705 -0
- package/dist/pt-Cus7eKQR.js.map +1 -0
- package/dist/types/analytics.d.ts +1 -1
- package/dist/types/collections.d.ts +31 -0
- package/dist/types/datasource.d.ts +67 -4
- package/dist/types/entities.d.ts +21 -1
- package/dist/types/entity_actions.d.ts +6 -0
- package/dist/types/entity_callbacks.d.ts +21 -0
- package/dist/types/firecms.d.ts +18 -1
- package/dist/types/firecms_context.d.ts +8 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/license.d.ts +55 -0
- package/dist/types/navigation.d.ts +34 -5
- package/dist/types/plugins.d.ts +22 -1
- package/dist/types/properties.d.ts +17 -1
- package/dist/types/side_entity_controller.d.ts +7 -0
- package/dist/types/translations.d.ts +43 -2
- package/dist/util/__tests__/collections.test.d.ts +1 -0
- package/dist/util/__tests__/urls.test.d.ts +1 -0
- package/dist/util/collections.d.ts +1 -1
- package/dist/util/date_fns_locales.d.ts +18 -0
- package/dist/util/entities.d.ts +1 -0
- package/dist/util/entity_cache.d.ts +11 -0
- package/dist/util/geopoint.d.ts +22 -0
- package/dist/util/index.d.ts +3 -0
- package/dist/util/lazy_eager.d.ts +3 -3
- package/dist/util/navigation_blocking.d.ts +22 -0
- package/dist/util/navigation_from_path.d.ts +17 -0
- package/dist/util/navigation_utils.d.ts +107 -3
- package/dist/util/parent_references_from_path.d.ts +16 -0
- package/dist/util/paths.d.ts +28 -0
- package/dist/util/permissions.d.ts +10 -4
- package/dist/util/urls.d.ts +22 -0
- package/package.json +17 -11
- package/src/app/Scaffold.tsx +41 -44
- package/src/app/useApp.tsx +1 -0
- package/src/components/DeleteEntityDialog.tsx +2 -0
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +2 -2
- package/src/components/EntityCollectionTable/EntityCollectionTable.tsx +3 -1
- package/src/components/EntityCollectionTable/column_utils.tsx +11 -19
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +1 -1
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +7 -0
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +5 -1
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +12 -0
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +66 -15
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +6 -2
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +8 -1
- package/src/components/EntityCollectionView/FiltersDialog.tsx +39 -28
- package/src/components/EntityCollectionView/useBoardDataController.tsx +17 -1
- package/src/components/EntityPreview.tsx +45 -41
- package/src/components/ErrorBoundary.tsx +12 -18
- package/src/components/LicenseBanner.tsx +207 -0
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +19 -2
- package/src/components/ReferenceWidget.tsx +1 -1
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +2 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +33 -7
- package/src/components/SelectableTable/filters/filter_text_input.test.ts +184 -0
- package/src/components/SelectableTable/filters/filter_text_input.ts +71 -0
- package/src/components/VirtualTable/VirtualTable.tsx +19 -8
- package/src/components/common/default_entity_actions.tsx +22 -2
- package/src/components/common/table_url_params.ts +172 -0
- package/src/components/common/useDataSourceTableController.tsx +126 -163
- package/src/components/common/useTableSearchHelper.ts +26 -2
- package/src/components/index.tsx +1 -0
- package/src/contexts/LicenseStatusContext.tsx +50 -0
- package/src/contexts/SnackbarProvider.tsx +14 -1
- package/src/core/DefaultAppBar.tsx +28 -16
- package/src/core/DefaultDrawer.tsx +150 -64
- package/src/core/DrawerNavigationGroup.tsx +27 -29
- package/src/core/DrawerNavigationItem.tsx +10 -12
- package/src/core/EntityEditView.tsx +77 -35
- package/src/core/EntityEditViewFormActions.tsx +3 -2
- package/src/core/EntitySidePanel.tsx +8 -4
- package/src/core/FireCMS.tsx +73 -47
- package/src/core/SideDialogs.tsx +44 -1
- package/src/core/field_configs.tsx +15 -0
- package/src/core/index.tsx +1 -0
- package/src/core/plugin_translations.ts +87 -0
- package/src/core/pro_plugins.ts +79 -0
- package/src/editor/components/SlashCommandMenu.tsx +4 -1
- package/src/editor/extensions/Image.ts +17 -1
- package/src/editor/hooks/useProseMirror.ts +5 -0
- package/src/editor/index.ts +11 -1
- package/src/editor/lazy_editor.tsx +33 -0
- package/src/form/EntityForm.tsx +31 -10
- package/src/form/EntityFormActions.tsx +2 -0
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +1 -1
- package/src/form/field_bindings/GeopointFieldBinding.tsx +139 -0
- package/src/form/index.tsx +1 -0
- package/src/hooks/data/delete.ts +8 -0
- package/src/hooks/data/save.ts +6 -1
- package/src/hooks/data/useCollectionFetch.tsx +21 -1
- package/src/hooks/data/useEntityFetch.tsx +35 -4
- package/src/hooks/index.tsx +2 -0
- package/src/hooks/useBuildNavigationController.tsx +70 -15
- package/src/hooks/useFireCMSContext.tsx +7 -3
- package/src/hooks/useLicenseStatus.tsx +32 -0
- package/src/hooks/useProjectLog.tsx +27 -9
- package/src/hooks/useResolvedNavigationFrom.tsx +3 -1
- package/src/hooks/useTranslation.ts +3 -1
- package/src/hooks/useValidateAuthenticator.tsx +4 -0
- package/src/i18n/FireCMSi18nProvider.tsx +199 -23
- package/src/i18n/__tests__/FireCMSi18nProvider.test.tsx +91 -0
- package/src/i18n/__tests__/nested_provider_lifecycle.test.tsx +67 -0
- package/src/internal/useBuildDataSource.ts +37 -15
- package/src/internal/useBuildSideEntityController.tsx +20 -5
- package/src/locales/de.ts +26 -4
- package/src/locales/en.ts +30 -6
- package/src/locales/es.ts +26 -4
- package/src/locales/fr.ts +26 -4
- package/src/locales/hi.ts +26 -4
- package/src/locales/it.ts +26 -4
- package/src/locales/pl.ts +752 -0
- package/src/locales/pt.ts +26 -4
- package/src/preview/PropertyPreview.tsx +12 -0
- package/src/preview/components/DatePreview.tsx +2 -4
- package/src/preview/components/ReferencePreview.tsx +7 -3
- package/src/preview/components/StorageThumbnail.tsx +24 -2
- package/src/preview/index.ts +1 -0
- package/src/preview/property_previews/GeopointPropertyPreview.tsx +23 -0
- package/src/routes/FireCMSRoute.tsx +54 -27
- package/src/types/analytics.ts +3 -0
- package/src/types/collections.ts +34 -0
- package/src/types/datasource.ts +67 -4
- package/src/types/entities.ts +24 -1
- package/src/types/entity_actions.tsx +6 -0
- package/src/types/entity_callbacks.ts +24 -0
- package/src/types/firecms.tsx +19 -1
- package/src/types/firecms_context.tsx +9 -0
- package/src/types/index.ts +1 -0
- package/src/types/license.ts +63 -0
- package/src/types/navigation.ts +35 -5
- package/src/types/plugins.tsx +23 -1
- package/src/types/properties.ts +17 -0
- package/src/types/side_entity_controller.tsx +7 -0
- package/src/types/translations.ts +43 -2
- package/src/util/__tests__/collections.test.ts +32 -0
- package/src/util/__tests__/urls.test.ts +131 -0
- package/src/util/collections.ts +1 -1
- package/src/util/date_fns_locales.ts +168 -0
- package/src/util/entities.ts +16 -1
- package/src/util/entity_cache.ts +18 -0
- package/src/util/geopoint.ts +81 -0
- package/src/util/index.ts +3 -0
- package/src/util/lazy_eager.tsx +30 -8
- package/src/util/navigation_blocking.ts +45 -0
- package/src/util/navigation_from_path.ts +47 -7
- package/src/util/navigation_utils.ts +250 -5
- package/src/util/parent_references_from_path.ts +45 -4
- package/src/util/paths.ts +40 -3
- package/src/util/permissions.ts +22 -10
- package/src/util/previews.ts +8 -1
- package/src/util/resolutions.ts +8 -0
- package/src/util/urls.ts +52 -0
- 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
|
-
|
|
354
|
-
|
|
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,13 +446,14 @@ 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
459
|
let overriddenCollection = baseCollection;
|
|
@@ -505,23 +547,33 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
505
547
|
throw Error("Expected path starting with " + fullCollectionPath);
|
|
506
548
|
}, [fullCollectionPath]);
|
|
507
549
|
|
|
508
|
-
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[] => {
|
|
509
556
|
const collections = collectionsRef.current ?? [];
|
|
510
|
-
return
|
|
557
|
+
return resolveCollectionPathSegments(pathSegments, collections);
|
|
511
558
|
}, []);
|
|
512
559
|
|
|
513
|
-
const getAllParentReferencesForPath = useCallback((path: string): EntityReference[] => {
|
|
560
|
+
const getAllParentReferencesForPath = useCallback((path: string, pathSegments?: string[]): EntityReference[] => {
|
|
514
561
|
const collections = collectionsRef.current ?? [];
|
|
515
562
|
return getParentReferencesFromPath({
|
|
516
563
|
path,
|
|
564
|
+
pathSegments,
|
|
517
565
|
collections
|
|
518
566
|
});
|
|
519
567
|
}, []);
|
|
520
568
|
|
|
521
|
-
const getParentCollectionIds = useCallback((path: string): string[] => {
|
|
569
|
+
const getParentCollectionIds = useCallback((path: string, pathSegments?: string[]): string[] => {
|
|
522
570
|
|
|
523
|
-
|
|
524
|
-
|
|
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);
|
|
525
577
|
oddPathSegments.pop();
|
|
526
578
|
|
|
527
579
|
const result: string[][] = [];
|
|
@@ -567,13 +619,16 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
567
619
|
urlPathToDataPath,
|
|
568
620
|
buildUrlCollectionPath,
|
|
569
621
|
resolveIdsFrom,
|
|
622
|
+
resolveSegmentsFrom,
|
|
570
623
|
topLevelNavigation,
|
|
571
624
|
refreshNavigation,
|
|
572
625
|
getParentReferencesFromPath: getAllParentReferencesForPath,
|
|
573
626
|
getParentCollectionIds,
|
|
574
627
|
convertIdsToPaths,
|
|
575
628
|
navigate,
|
|
576
|
-
|
|
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
|
|
577
632
|
};
|
|
578
633
|
}
|
|
579
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)
|
|
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,
|
|
@@ -1,17 +1,76 @@
|
|
|
1
|
-
import React, { PropsWithChildren, useEffect, useRef } from "react";
|
|
1
|
+
import React, { PropsWithChildren, useContext, useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import i18next, { i18n } from "i18next";
|
|
3
|
-
import { I18nextProvider, initReactI18next } from "react-i18next";
|
|
3
|
+
import { I18nContext, I18nextProvider, initReactI18next } from "react-i18next";
|
|
4
4
|
import { en } from "../locales/en";
|
|
5
5
|
import { es } from "../locales/es";
|
|
6
|
-
import { de } from "../locales/de";
|
|
7
|
-
import { fr } from "../locales/fr";
|
|
8
|
-
import { it } from "../locales/it";
|
|
9
|
-
import { hi } from "../locales/hi";
|
|
10
|
-
import { pt } from "../locales/pt";
|
|
11
6
|
import { FireCMSTranslations } from "../types/translations";
|
|
12
7
|
|
|
13
8
|
const FIRECMS_NS = "firecms_core";
|
|
14
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Translations that are not part of the bundle every app downloads.
|
|
12
|
+
*
|
|
13
|
+
* All eight locales used to be imported statically, which put ~305KB of strings
|
|
14
|
+
* — every language, for every user — into `@firecms/core` and therefore onto the
|
|
15
|
+
* startup path of every FireCMS app. Only the active language is ever read.
|
|
16
|
+
*
|
|
17
|
+
* `en` and `es` stay static: `en` is the fallback language and the base every
|
|
18
|
+
* unknown locale is seeded from, and both are re-exported from the package root,
|
|
19
|
+
* so a consumer may be importing them by name.
|
|
20
|
+
*
|
|
21
|
+
* The specifiers are written out rather than built from a template so bundlers
|
|
22
|
+
* can see them and emit one chunk per locale.
|
|
23
|
+
*/
|
|
24
|
+
const localeLoaders: Record<string, () => Promise<FireCMSTranslations>> = {
|
|
25
|
+
de: () => import("../locales/de").then((m) => m.de),
|
|
26
|
+
fr: () => import("../locales/fr").then((m) => m.fr),
|
|
27
|
+
it: () => import("../locales/it").then((m) => m.it),
|
|
28
|
+
hi: () => import("../locales/hi").then((m) => m.hi),
|
|
29
|
+
pt: () => import("../locales/pt").then((m) => m.pt),
|
|
30
|
+
pl: () => import("../locales/pl").then((m) => m.pl)
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Locale bundles available synchronously: the two static ones plus anything a
|
|
35
|
+
* previous provider already fetched. `buildResources` reads this, so a rebuild
|
|
36
|
+
* triggered by a changing `translations` prop cannot overwrite a lazily loaded
|
|
37
|
+
* language with the English base it would otherwise fall back to.
|
|
38
|
+
*/
|
|
39
|
+
const loadedLocales: Record<string, FireCMSTranslations> = {
|
|
40
|
+
en,
|
|
41
|
+
es
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const inFlightLocales: Record<string, Promise<void>> = {};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Fetch a locale bundle into `loadedLocales`.
|
|
48
|
+
*
|
|
49
|
+
* Returns undefined when the bundle is already in memory or there is nothing to
|
|
50
|
+
* fetch, so callers can stay synchronous in the common case.
|
|
51
|
+
*/
|
|
52
|
+
function loadLocale(locale?: string): Promise<void> | undefined {
|
|
53
|
+
if (!locale) return undefined;
|
|
54
|
+
const language = locale.split("-")[0];
|
|
55
|
+
if (loadedLocales[language]) return undefined;
|
|
56
|
+
const loader = localeLoaders[language];
|
|
57
|
+
if (!loader) return undefined;
|
|
58
|
+
if (!inFlightLocales[language]) {
|
|
59
|
+
inFlightLocales[language] = loader()
|
|
60
|
+
.then((bundle) => {
|
|
61
|
+
loadedLocales[language] = bundle;
|
|
62
|
+
})
|
|
63
|
+
.catch(() => {
|
|
64
|
+
// A locale that fails to load falls back to English, which is
|
|
65
|
+
// what an unknown locale has always done.
|
|
66
|
+
})
|
|
67
|
+
.finally(() => {
|
|
68
|
+
delete inFlightLocales[language];
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return inFlightLocales[language];
|
|
72
|
+
}
|
|
73
|
+
|
|
15
74
|
export const FIRECMS_LOCALE_STORAGE_KEY = "firecms_locale";
|
|
16
75
|
|
|
17
76
|
/** DeepPartial helper — allows partial overrides at any nesting level */
|
|
@@ -49,21 +108,44 @@ export function FireCMSi18nProvider({
|
|
|
49
108
|
translations,
|
|
50
109
|
children
|
|
51
110
|
}: PropsWithChildren<FireCMSi18nProviderProps>) {
|
|
111
|
+
|
|
112
|
+
// A FireCMS i18next instance already in context means this provider is nested —
|
|
113
|
+
// e.g. FireCMSCloudApp mounts one inside the one the host app already mounted.
|
|
114
|
+
// A second instance built only from the defaults would shadow the parent's and
|
|
115
|
+
// silently drop every translation the host registered, leaving the host's own
|
|
116
|
+
// strings to render as raw keys.
|
|
117
|
+
//
|
|
118
|
+
// So the nested instance is *seeded* from the parent's resources rather than
|
|
119
|
+
// sharing them. Sharing one mutable instance was the obvious alternative and is
|
|
120
|
+
// wrong: bundles added by one provider outlive it, so opening one project's app
|
|
121
|
+
// and then switching to another left the first project's translations answering
|
|
122
|
+
// for keys the second never defined. Copying keeps each provider's strings to
|
|
123
|
+
// itself, which is also how this behaved before nesting was handled at all.
|
|
124
|
+
const parentI18n = useContext(I18nContext)?.i18n;
|
|
125
|
+
const parentInstance = parentI18n?.hasResourceBundle?.("en", FIRECMS_NS)
|
|
126
|
+
? parentI18n
|
|
127
|
+
: undefined;
|
|
128
|
+
|
|
52
129
|
const i18nRef = useRef<i18n | null>(null);
|
|
53
130
|
const [ready, setReady] = React.useState(false);
|
|
54
131
|
|
|
55
132
|
if (!i18nRef.current) {
|
|
56
133
|
const instance = i18next.createInstance();
|
|
57
134
|
|
|
58
|
-
//
|
|
59
|
-
|
|
135
|
+
// English baseline + this provider's overrides, with anything the parent
|
|
136
|
+
// already resolved layered in underneath the overrides.
|
|
137
|
+
const resources = buildResourcesWithParent(translations, parentInstance);
|
|
60
138
|
|
|
61
|
-
let initialLocale = locale;
|
|
139
|
+
let initialLocale = parentInstance?.language ?? locale;
|
|
62
140
|
if (typeof window !== "undefined") {
|
|
63
141
|
const stored = localStorage.getItem(FIRECMS_LOCALE_STORAGE_KEY);
|
|
64
142
|
if (stored) initialLocale = stored;
|
|
65
143
|
}
|
|
66
144
|
|
|
145
|
+
// Kicked off before init so the request is in flight while i18next sets
|
|
146
|
+
// itself up, rather than after it.
|
|
147
|
+
const pendingInitialLocale = loadLocale(initialLocale);
|
|
148
|
+
|
|
67
149
|
instance
|
|
68
150
|
.use(initReactI18next)
|
|
69
151
|
.init({
|
|
@@ -76,19 +158,54 @@ export function FireCMSi18nProvider({
|
|
|
76
158
|
// React already escapes — don't double-escape
|
|
77
159
|
escapeValue: false,
|
|
78
160
|
},
|
|
161
|
+
react: {
|
|
162
|
+
// Locale bundles now arrive after init, and a bundle landing
|
|
163
|
+
// in the store raises an `added` event rather than
|
|
164
|
+
// `languageChanged`. react-i18next binds only the latter by
|
|
165
|
+
// default, so without this a language switched from the
|
|
166
|
+
// toggle would fetch its strings, put them in the store, and
|
|
167
|
+
// leave the UI sitting on the English fallback.
|
|
168
|
+
bindI18nStore: "added"
|
|
169
|
+
},
|
|
79
170
|
}, () => {
|
|
80
|
-
|
|
171
|
+
if (!pendingInitialLocale) {
|
|
172
|
+
setReady(true);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Holding the first render until the active language has arrived
|
|
176
|
+
// keeps a non-English user from seeing English strings repaint.
|
|
177
|
+
// Only locales outside the static pair wait, and only on the
|
|
178
|
+
// first mount.
|
|
179
|
+
pendingInitialLocale.then(() => {
|
|
180
|
+
applyLoadedLocale(instance, initialLocale, translations, parentInstance);
|
|
181
|
+
setReady(true);
|
|
182
|
+
});
|
|
81
183
|
});
|
|
82
184
|
|
|
83
185
|
instance.on("languageChanged", (lng) => {
|
|
84
186
|
if (typeof window !== "undefined") {
|
|
85
187
|
localStorage.setItem(FIRECMS_LOCALE_STORAGE_KEY, lng);
|
|
86
188
|
}
|
|
189
|
+
// Switching to a language whose bundle has not been fetched yet
|
|
190
|
+
// renders the English fallback until it lands.
|
|
191
|
+
loadLocale(lng)?.then(() => applyLoadedLocale(instance, lng, translations, parentInstance));
|
|
87
192
|
});
|
|
88
193
|
|
|
89
194
|
i18nRef.current = instance;
|
|
90
195
|
}
|
|
91
196
|
|
|
197
|
+
// Follow the parent's language, so a switch made outside this provider is not
|
|
198
|
+
// stranded on the other side of the boundary.
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (!parentInstance || !i18nRef.current) return;
|
|
201
|
+
const instance = i18nRef.current;
|
|
202
|
+
const follow = (lng: string) => {
|
|
203
|
+
if (instance.language !== lng) instance.changeLanguage(lng);
|
|
204
|
+
};
|
|
205
|
+
parentInstance.on("languageChanged", follow);
|
|
206
|
+
return () => { parentInstance.off("languageChanged", follow); };
|
|
207
|
+
}, [parentInstance]);
|
|
208
|
+
|
|
92
209
|
// When `locale` prop changes, switch language on the existing instance
|
|
93
210
|
// ONLY if the user hasn't explicitly set a preference
|
|
94
211
|
useEffect(() => {
|
|
@@ -100,10 +217,16 @@ export function FireCMSi18nProvider({
|
|
|
100
217
|
}
|
|
101
218
|
}, [locale]);
|
|
102
219
|
|
|
103
|
-
// When consumer translations prop changes, update the resource bundles
|
|
220
|
+
// When consumer translations prop changes, update the resource bundles.
|
|
221
|
+
//
|
|
222
|
+
// Built through the same layering as the initial instance, parent included.
|
|
223
|
+
// Rebuilding from the bare English baseline here would overwrite whatever the
|
|
224
|
+
// host registered — a nested provider without translations of its own would
|
|
225
|
+
// reset the host's overrides to the built-in strings. That was already true;
|
|
226
|
+
// it was simply invisible until `bindI18nStore` made a store write repaint.
|
|
104
227
|
useEffect(() => {
|
|
105
228
|
if (!i18nRef.current) return;
|
|
106
|
-
const resources =
|
|
229
|
+
const resources = buildResourcesWithParent(translations, parentInstance);
|
|
107
230
|
for (const [lang, bundle] of Object.entries(resources)) {
|
|
108
231
|
i18nRef.current.addResourceBundle(
|
|
109
232
|
lang,
|
|
@@ -113,7 +236,7 @@ export function FireCMSi18nProvider({
|
|
|
113
236
|
true // overwrite existing keys
|
|
114
237
|
);
|
|
115
238
|
}
|
|
116
|
-
}, [translations]);
|
|
239
|
+
}, [translations, parentInstance]);
|
|
117
240
|
|
|
118
241
|
if (!ready || !i18nRef.current) return null;
|
|
119
242
|
|
|
@@ -124,6 +247,64 @@ export function FireCMSi18nProvider({
|
|
|
124
247
|
);
|
|
125
248
|
}
|
|
126
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Push a lazily loaded locale bundle into a live i18next instance.
|
|
252
|
+
*
|
|
253
|
+
* Consumer overrides are re-applied on top, because the instance may already be
|
|
254
|
+
* holding a bundle that `buildResources` seeded from English for this language,
|
|
255
|
+
* and the real translations have to win over that seed while the overrides win
|
|
256
|
+
* over both.
|
|
257
|
+
*/
|
|
258
|
+
function applyLoadedLocale(
|
|
259
|
+
instance: i18n,
|
|
260
|
+
locale: string,
|
|
261
|
+
translations?: { [locale: string]: DeepPartial<FireCMSTranslations> },
|
|
262
|
+
parentInstance?: i18n
|
|
263
|
+
) {
|
|
264
|
+
const language = locale.split("-")[0];
|
|
265
|
+
if (!loadedLocales[language]) return;
|
|
266
|
+
const bundle = buildResourcesWithParent(translations, parentInstance)[language]?.[FIRECMS_NS];
|
|
267
|
+
if (!bundle) return;
|
|
268
|
+
instance.addResourceBundle(
|
|
269
|
+
language,
|
|
270
|
+
FIRECMS_NS,
|
|
271
|
+
bundle,
|
|
272
|
+
true, // deep merge
|
|
273
|
+
true // overwrite the English seed
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The full resource map for this provider: the locale bundles currently in
|
|
279
|
+
* memory, then anything the parent provider already resolved, then this
|
|
280
|
+
* provider's own overrides on top.
|
|
281
|
+
*
|
|
282
|
+
* Shared by every path that writes into the instance — initial construction, a
|
|
283
|
+
* changed `translations` prop, and a locale arriving late — so all three layer
|
|
284
|
+
* in the same order and none of them can undo another.
|
|
285
|
+
*/
|
|
286
|
+
function buildResourcesWithParent(
|
|
287
|
+
translations?: { [locale: string]: DeepPartial<FireCMSTranslations> },
|
|
288
|
+
parentInstance?: i18n
|
|
289
|
+
): Record<string, Record<string, object>> {
|
|
290
|
+
const resources = buildResources(translations);
|
|
291
|
+
if (!parentInstance) return resources;
|
|
292
|
+
|
|
293
|
+
const inherited: Record<string, any> = parentInstance.services?.resourceStore?.data ?? {};
|
|
294
|
+
for (const [lang, namespaces] of Object.entries(inherited)) {
|
|
295
|
+
const bundle = (namespaces as any)?.[FIRECMS_NS];
|
|
296
|
+
if (!bundle) continue;
|
|
297
|
+
resources[lang] = {
|
|
298
|
+
[FIRECMS_NS]: {
|
|
299
|
+
...(resources[lang]?.[FIRECMS_NS] ?? {}),
|
|
300
|
+
...bundle,
|
|
301
|
+
...(translations?.[lang] ?? {})
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return resources;
|
|
306
|
+
}
|
|
307
|
+
|
|
127
308
|
/**
|
|
128
309
|
* Build an i18next resources object from the English baseline plus any
|
|
129
310
|
* consumer-provided overrides.
|
|
@@ -131,15 +312,10 @@ export function FireCMSi18nProvider({
|
|
|
131
312
|
function buildResources(
|
|
132
313
|
translations?: { [locale: string]: DeepPartial<FireCMSTranslations> }
|
|
133
314
|
): Record<string, Record<string, object>> {
|
|
134
|
-
const resources: Record<string, Record<string, object>> = {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
fr: { [FIRECMS_NS]: { ...fr } },
|
|
139
|
-
it: { [FIRECMS_NS]: { ...it } },
|
|
140
|
-
hi: { [FIRECMS_NS]: { ...hi } },
|
|
141
|
-
pt: { [FIRECMS_NS]: { ...pt } },
|
|
142
|
-
};
|
|
315
|
+
const resources: Record<string, Record<string, object>> = {};
|
|
316
|
+
for (const [lang, bundle] of Object.entries(loadedLocales)) {
|
|
317
|
+
resources[lang] = { [FIRECMS_NS]: { ...bundle } };
|
|
318
|
+
}
|
|
143
319
|
|
|
144
320
|
if (!translations) return resources;
|
|
145
321
|
|