@firecms/core 3.1.0 → 3.2.0-canary.4c3b8f2
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/dist/components/EntityCollectionView/CollectionDataErrorBanner.d.ts +4 -0
- package/dist/components/ErrorBoundary.d.ts +3 -1
- package/dist/components/HomePage/DefaultHomePage.d.ts +0 -1
- package/dist/components/LanguageToggle.d.ts +1 -0
- package/dist/components/UnsavedChangesDialog.d.ts +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/core/DrawerNavigationGroup.d.ts +2 -2
- package/dist/editor/components/SlashCommandMenu.d.ts +6 -0
- package/dist/editor/components/editor-bubble-item.d.ts +8 -0
- package/dist/editor/components/editor-bubble.d.ts +8 -0
- package/dist/editor/components/image-bubble.d.ts +5 -0
- package/dist/editor/components/index.d.ts +16 -0
- package/dist/editor/components/table-bubble.d.ts +5 -0
- package/dist/editor/editor.d.ts +30 -0
- package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
- package/dist/editor/extensions/Image/index.d.ts +6 -0
- package/dist/editor/extensions/Image.d.ts +6 -0
- package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
- package/dist/editor/extensions/clipboard.d.ts +7 -0
- package/dist/editor/extensions/custom-keymap.d.ts +1 -0
- package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
- package/dist/editor/hooks/useProseMirror.d.ts +13 -0
- package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
- package/dist/editor/index.d.ts +2 -0
- package/dist/editor/markdown.d.ts +5 -0
- package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
- package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
- package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
- package/dist/editor/nodeViews/index.d.ts +6 -0
- package/dist/editor/plugins/index.d.ts +2 -0
- package/dist/editor/plugins/inputrules.d.ts +6 -0
- package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
- package/dist/editor/plugins/slashCommandPlugin.d.ts +12 -0
- package/dist/editor/schema.d.ts +2 -0
- package/dist/editor/selectors/ai-selector.d.ts +0 -0
- package/dist/editor/selectors/color-selector.d.ts +10 -0
- package/dist/editor/selectors/link-selector.d.ts +8 -0
- package/dist/editor/selectors/node-selector.d.ts +15 -0
- package/dist/editor/selectors/text-buttons.d.ts +1 -0
- package/dist/editor/types.d.ts +5 -0
- package/dist/editor/useProseMirror.d.ts +16 -0
- package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
- package/dist/editor/utils/remove_classes.d.ts +1 -0
- package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useBuildNavigationController.d.ts +0 -1
- package/dist/hooks/useCollapsedGroups.d.ts +3 -3
- package/dist/hooks/useTranslation.d.ts +17 -0
- package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.es.js +12898 -2265
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +12877 -2264
- package/dist/index.umd.js.map +1 -1
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/en.d.ts +10 -0
- package/dist/locales/es.d.ts +10 -0
- package/dist/locales/fr.d.ts +2 -0
- package/dist/locales/hi.d.ts +2 -0
- package/dist/locales/it.d.ts +2 -0
- package/dist/locales/pt.d.ts +7 -0
- package/dist/types/customization_controller.d.ts +2 -1
- package/dist/types/firecms.d.ts +2 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/navigation.d.ts +2 -2
- package/dist/types/plugins.d.ts +7 -0
- package/dist/types/storage.d.ts +1 -0
- package/dist/types/translations.d.ts +646 -0
- package/dist/util/useStorageUploadController.d.ts +10 -1
- package/package.json +45 -9
- package/src/app/Scaffold.tsx +7 -5
- package/src/components/AIIcon.tsx +3 -1
- package/src/components/ArrayContainer.tsx +6 -4
- package/src/components/ClearFilterSortButton.tsx +6 -3
- package/src/components/ConfirmationDialog.tsx +4 -2
- package/src/components/DeleteEntityDialog.tsx +10 -7
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
- package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
- package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +16 -43
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +26 -18
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
- package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
- package/src/components/EntityCollectionView/ViewModeToggle.tsx +11 -8
- package/src/components/EntityView.tsx +3 -2
- package/src/components/ErrorBoundary.tsx +27 -15
- package/src/components/HomePage/DefaultHomePage.tsx +19 -13
- package/src/components/HomePage/HomePageDnD.tsx +3 -1
- package/src/components/HomePage/NavigationGroup.tsx +3 -1
- package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
- package/src/components/LanguageToggle.tsx +66 -0
- package/src/components/NotFoundPage.tsx +5 -3
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
- package/src/components/ReferenceWidget.tsx +3 -2
- package/src/components/SearchIconsView.tsx +3 -1
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
- package/src/components/UnsavedChangesDialog.tsx +6 -4
- package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
- package/src/components/VirtualTable/VirtualTableHeader.tsx +12 -10
- package/src/components/common/default_entity_actions.tsx +4 -0
- package/src/components/common/useDataSourceTableController.tsx +12 -4
- package/src/components/index.tsx +1 -0
- package/src/core/DefaultAppBar.tsx +14 -10
- package/src/core/DefaultDrawer.tsx +8 -2
- package/src/core/DrawerNavigationGroup.tsx +5 -3
- package/src/core/EntityEditView.tsx +4 -3
- package/src/core/EntityEditViewFormActions.tsx +24 -17
- package/src/core/EntitySidePanel.tsx +6 -5
- package/src/core/FireCMS.tsx +33 -6
- package/src/editor/components/SlashCommandMenu.tsx +516 -0
- package/src/editor/components/editor-bubble-item.tsx +32 -0
- package/src/editor/components/editor-bubble.tsx +118 -0
- package/src/editor/components/image-bubble.tsx +156 -0
- package/src/editor/components/index.ts +14 -0
- package/src/editor/components/table-bubble.tsx +165 -0
- package/src/editor/editor.tsx +455 -0
- package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
- package/src/editor/extensions/Image/index.ts +133 -0
- package/src/editor/extensions/Image.ts +159 -0
- package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
- package/src/editor/extensions/clipboard.ts +72 -0
- package/src/editor/extensions/custom-keymap.ts +24 -0
- package/src/editor/extensions/drag-and-drop.tsx +480 -0
- package/src/editor/hooks/useProseMirror.ts +124 -0
- package/src/editor/hooks/useProseMirrorContext.ts +15 -0
- package/src/editor/index.ts +2 -0
- package/src/editor/markdown.ts +172 -0
- package/src/editor/nodeViews/ImageComponent.tsx +20 -0
- package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
- package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
- package/src/editor/nodeViews/index.ts +35 -0
- package/src/editor/plugins/index.ts +58 -0
- package/src/editor/plugins/inputrules.ts +82 -0
- package/src/editor/plugins/placeholderPlugin.ts +55 -0
- package/src/editor/plugins/slashCommandPlugin.ts +61 -0
- package/src/editor/schema.ts +240 -0
- package/src/editor/selectors/ai-selector.tsx +111 -0
- package/src/editor/selectors/color-selector.tsx +200 -0
- package/src/editor/selectors/link-selector.tsx +118 -0
- package/src/editor/selectors/node-selector.tsx +157 -0
- package/src/editor/selectors/text-buttons.tsx +86 -0
- package/src/editor/types.ts +6 -0
- package/src/editor/useProseMirror.ts +126 -0
- package/src/editor/utils/prosemirror-utils.ts +108 -0
- package/src/editor/utils/remove_classes.ts +17 -0
- package/src/editor/utils/useDebouncedCallback.ts +25 -0
- package/src/form/EntityForm.tsx +16 -3
- package/src/form/EntityFormActions.tsx +19 -12
- package/src/form/PropertyFieldBinding.tsx +3 -2
- package/src/form/components/LocalChangesMenu.tsx +13 -13
- package/src/form/components/StorageItemPreview.tsx +3 -2
- package/src/form/components/StorageUploadProgress.tsx +18 -3
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -4
- package/src/form/field_bindings/BlockFieldBinding.tsx +5 -2
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -18
- package/src/form/field_bindings/MapFieldBinding.tsx +4 -3
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +33 -19
- package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -1
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +4 -3
- package/src/hooks/index.tsx +1 -0
- package/src/hooks/useBuildNavigationController.tsx +45 -18
- package/src/hooks/useCollapsedGroups.ts +7 -6
- package/src/hooks/useTranslation.ts +31 -0
- package/src/i18n/FireCMSi18nProvider.tsx +160 -0
- package/src/index.ts +4 -0
- package/src/internal/useBuildSideEntityController.tsx +22 -20
- package/src/locales/de.ts +691 -0
- package/src/locales/en.ts +703 -0
- package/src/locales/es.ts +703 -0
- package/src/locales/fr.ts +691 -0
- package/src/locales/hi.ts +691 -0
- package/src/locales/it.ts +691 -0
- package/src/locales/pt.ts +700 -0
- package/src/preview/components/UrlComponentPreview.tsx +4 -2
- package/src/preview/components/UserPreview.tsx +3 -1
- package/src/types/customization_controller.tsx +2 -1
- package/src/types/firecms.tsx +2 -1
- package/src/types/index.ts +1 -0
- package/src/types/navigation.ts +2 -2
- package/src/types/plugins.tsx +8 -0
- package/src/types/properties.ts +1 -0
- package/src/types/storage.ts +2 -1
- package/src/types/translations.ts +725 -0
- package/src/util/useStorageUploadController.tsx +23 -29
|
@@ -34,7 +34,6 @@ import { getParentReferencesFromPath } from "../util/parent_references_from_path
|
|
|
34
34
|
const DEFAULT_BASE_PATH = "/";
|
|
35
35
|
const DEFAULT_COLLECTION_PATH = "/c";
|
|
36
36
|
|
|
37
|
-
export const NAVIGATION_DEFAULT_GROUP_NAME = "Views";
|
|
38
37
|
export const NAVIGATION_ADMIN_GROUP_NAME = "Admin";
|
|
39
38
|
|
|
40
39
|
export type BuildNavigationContextProps<EC extends EntityCollection, USER extends User> = {
|
|
@@ -189,7 +188,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
189
188
|
path: pathKey,
|
|
190
189
|
collection,
|
|
191
190
|
description: collection.description?.trim(),
|
|
192
|
-
group: groupName
|
|
191
|
+
group: groupName
|
|
193
192
|
});
|
|
194
193
|
return acc;
|
|
195
194
|
}, [] as NavigationEntry[]),
|
|
@@ -217,7 +216,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
217
216
|
path: view.path,
|
|
218
217
|
view,
|
|
219
218
|
description: view.description?.trim(),
|
|
220
|
-
group: groupName
|
|
219
|
+
group: groupName
|
|
221
220
|
});
|
|
222
221
|
return acc;
|
|
223
222
|
}, [] as NavigationEntry[]),
|
|
@@ -242,7 +241,7 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
242
241
|
}, [] as NavigationEntry[])
|
|
243
242
|
];
|
|
244
243
|
|
|
245
|
-
const groupOrderValue = (groupName?: string): number => {
|
|
244
|
+
const groupOrderValue = (groupName?: string | null): number => {
|
|
246
245
|
if (groupName === NAVIGATION_ADMIN_GROUP_NAME) return 1;
|
|
247
246
|
return 0; // Other groups
|
|
248
247
|
};
|
|
@@ -266,7 +265,10 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
266
265
|
|
|
267
266
|
const collectedGroupsFromEntries = navigationEntries
|
|
268
267
|
.map(e => e.group)
|
|
269
|
-
.filter(
|
|
268
|
+
.filter((g): g is string => g !== null && Boolean(g));
|
|
269
|
+
|
|
270
|
+
// Check if there are any ungrouped entries
|
|
271
|
+
const hasUngroupedEntries = navigationEntries.some(e => e.group === null && e.type !== "admin");
|
|
270
272
|
|
|
271
273
|
// Preserve order from finalNavigationGroupMappings (persisted order)
|
|
272
274
|
const groupsFromMappings = finalNavigationGroupMappings.map(g => g.name);
|
|
@@ -284,7 +286,12 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
284
286
|
const uniqueGroupsArray = [...new Set(allDefinedGroups)];
|
|
285
287
|
const adminGroups = uniqueGroupsArray.filter(g => g === NAVIGATION_ADMIN_GROUP_NAME);
|
|
286
288
|
const nonAdminGroups = uniqueGroupsArray.filter(g => g !== NAVIGATION_ADMIN_GROUP_NAME);
|
|
287
|
-
|
|
289
|
+
// Place null (ungrouped) first if there are ungrouped entries
|
|
290
|
+
const uniqueGroups: (string | null)[] = [
|
|
291
|
+
...(hasUngroupedEntries ? [null] : []),
|
|
292
|
+
...nonAdminGroups,
|
|
293
|
+
...adminGroups
|
|
294
|
+
];
|
|
288
295
|
|
|
289
296
|
return {
|
|
290
297
|
allowDragAndDrop: plugins?.some(plugin => plugin.homePage?.allowDragAndDrop) ?? false,
|
|
@@ -487,8 +494,10 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
487
494
|
|
|
488
495
|
const urlPathToDataPath = useCallback((path: string): string => {
|
|
489
496
|
const decodedPath = decodeURIComponent(path);
|
|
490
|
-
|
|
491
|
-
|
|
497
|
+
const withoutHash = decodedPath.split("#")[0];
|
|
498
|
+
const cleanPath = withoutHash.split("?")[0];
|
|
499
|
+
if (cleanPath.startsWith(fullCollectionPath))
|
|
500
|
+
return cleanPath.replace(fullCollectionPath, "");
|
|
492
501
|
throw Error("Expected path starting with " + fullCollectionPath);
|
|
493
502
|
}, [fullCollectionPath]);
|
|
494
503
|
|
|
@@ -565,9 +574,27 @@ export function useBuildNavigationController<EC extends EntityCollection, USER e
|
|
|
565
574
|
}
|
|
566
575
|
|
|
567
576
|
function encodePath(input: string) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
577
|
+
const cleanInput = removeInitialAndTrailingSlashes(input);
|
|
578
|
+
const [pathPart, rest] = cleanInput.split("?", 2);
|
|
579
|
+
|
|
580
|
+
let encodedPath = encodeURIComponent(pathPart).replaceAll("%2F", "/");
|
|
581
|
+
let result = encodedPath;
|
|
582
|
+
|
|
583
|
+
if (rest !== undefined) {
|
|
584
|
+
const [searchPart, hashPart] = rest.split("#", 2);
|
|
585
|
+
result += `?${searchPart}`;
|
|
586
|
+
if (hashPart !== undefined) {
|
|
587
|
+
result += `#${hashPart}`;
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
const [pathOnly, hashOnly] = cleanInput.split("#", 2);
|
|
591
|
+
if (hashOnly !== undefined) {
|
|
592
|
+
encodedPath = encodeURIComponent(pathOnly).replaceAll("%2F", "/");
|
|
593
|
+
result = `${encodedPath}#${hashOnly}`;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return result;
|
|
571
598
|
}
|
|
572
599
|
|
|
573
600
|
function filterOutNotAllowedCollections(resolvedCollections: EntityCollection[], authController: AuthController<User>): EntityCollection[] {
|
|
@@ -662,12 +689,12 @@ async function resolveCMSViews(
|
|
|
662
689
|
return resolvedViews;
|
|
663
690
|
}
|
|
664
691
|
|
|
665
|
-
function getGroup(collectionOrView: EntityCollection<any, any> | CMSView) {
|
|
692
|
+
function getGroup(collectionOrView: EntityCollection<any, any> | CMSView): string | null {
|
|
666
693
|
const trimmed = collectionOrView.group?.trim();
|
|
667
694
|
if (!trimmed || trimmed === "") {
|
|
668
|
-
return
|
|
695
|
+
return null;
|
|
669
696
|
}
|
|
670
|
-
return trimmed
|
|
697
|
+
return trimmed;
|
|
671
698
|
}
|
|
672
699
|
|
|
673
700
|
function areCollectionListsEqual(a: EntityCollection[], b: EntityCollection[]) {
|
|
@@ -803,7 +830,7 @@ function computeNavigationGroups({
|
|
|
803
830
|
(collections ?? []).forEach(collection => {
|
|
804
831
|
const entry = collection.id ?? collection.path;
|
|
805
832
|
if (!assignedEntries.has(entry)) {
|
|
806
|
-
const groupName = getGroup(collection);
|
|
833
|
+
const groupName = getGroup(collection) ?? "__default__";
|
|
807
834
|
if (!unassignedGroupMap[groupName]) unassignedGroupMap[groupName] = [];
|
|
808
835
|
unassignedGroupMap[groupName].push(entry);
|
|
809
836
|
}
|
|
@@ -813,7 +840,7 @@ function computeNavigationGroups({
|
|
|
813
840
|
(views ?? []).forEach(view => {
|
|
814
841
|
const entry = view.path;
|
|
815
842
|
if (!assignedEntries.has(entry)) {
|
|
816
|
-
const groupName = getGroup(view);
|
|
843
|
+
const groupName = getGroup(view) ?? "__default__";
|
|
817
844
|
if (!unassignedGroupMap[groupName]) unassignedGroupMap[groupName] = [];
|
|
818
845
|
unassignedGroupMap[groupName].push(entry);
|
|
819
846
|
}
|
|
@@ -841,7 +868,7 @@ function computeNavigationGroups({
|
|
|
841
868
|
|
|
842
869
|
// Add collections
|
|
843
870
|
(collections ?? []).forEach(collection => {
|
|
844
|
-
const name = getGroup(collection);
|
|
871
|
+
const name = getGroup(collection) ?? "__default__";
|
|
845
872
|
const entry = collection.id ?? collection.path;
|
|
846
873
|
if (!groupMap[name]) groupMap[name] = [];
|
|
847
874
|
groupMap[name].push(entry);
|
|
@@ -849,7 +876,7 @@ function computeNavigationGroups({
|
|
|
849
876
|
|
|
850
877
|
// Add views
|
|
851
878
|
(views ?? []).forEach(view => {
|
|
852
|
-
const name = getGroup(view);
|
|
879
|
+
const name = getGroup(view) ?? "__default__";
|
|
853
880
|
const entry = view.path;
|
|
854
881
|
if (!groupMap[name]) groupMap[name] = [];
|
|
855
882
|
groupMap[name].push(entry);
|
|
@@ -10,7 +10,7 @@ const STORAGE_KEY_PREFIX = "firecms-collapsed-groups";
|
|
|
10
10
|
* @param groupNames - Array of group names to track
|
|
11
11
|
* @param namespace - Namespace for localStorage key (e.g., "home", "drawer") to allow independent state
|
|
12
12
|
*/
|
|
13
|
-
export function useCollapsedGroups(groupNames: string[], namespace: string = "default") {
|
|
13
|
+
export function useCollapsedGroups(groupNames: (string | null)[], namespace: string = "default") {
|
|
14
14
|
const storageKey = `${STORAGE_KEY_PREFIX}-${namespace}`;
|
|
15
15
|
|
|
16
16
|
// Load collapsed groups from localStorage on mount
|
|
@@ -37,7 +37,7 @@ export function useCollapsedGroups(groupNames: string[], namespace: string = "de
|
|
|
37
37
|
// Only clean up if we have actual groups loaded (avoid cleaning up during initial load)
|
|
38
38
|
if (groupNames.length === 0) return;
|
|
39
39
|
|
|
40
|
-
const currentGroupNames = new Set(groupNames);
|
|
40
|
+
const currentGroupNames = new Set(groupNames.map(g => g ?? "__default__"));
|
|
41
41
|
|
|
42
42
|
setCollapsedGroups(prev => {
|
|
43
43
|
const cleaned = Object.fromEntries(
|
|
@@ -56,12 +56,13 @@ export function useCollapsedGroups(groupNames: string[], namespace: string = "de
|
|
|
56
56
|
});
|
|
57
57
|
}, [groupNames]);
|
|
58
58
|
|
|
59
|
-
const isGroupCollapsed = useCallback((name
|
|
60
|
-
return !!collapsedGroups[name];
|
|
59
|
+
const isGroupCollapsed = useCallback((name?: string | null) => {
|
|
60
|
+
return !!collapsedGroups[name ?? "__default__"];
|
|
61
61
|
}, [collapsedGroups]);
|
|
62
62
|
|
|
63
|
-
const toggleGroupCollapsed = useCallback((name
|
|
64
|
-
|
|
63
|
+
const toggleGroupCollapsed = useCallback((name?: string | null) => {
|
|
64
|
+
const key = name ?? "__default__";
|
|
65
|
+
setCollapsedGroups(prev => ({ ...prev, [key]: !prev[key] }));
|
|
65
66
|
}, []);
|
|
66
67
|
|
|
67
68
|
return {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useTranslation as useI18nTranslation } from "react-i18next";
|
|
2
|
+
|
|
3
|
+
const FIRECMS_NS = "firecms_core";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal hook for translating FireCMS UI strings.
|
|
7
|
+
*
|
|
8
|
+
* Uses the `firecms_core` i18next namespace that is initialised by
|
|
9
|
+
* `FireCMSi18nProvider`. Do NOT use `react-i18next` directly in internal
|
|
10
|
+
* components — always go through this hook so the namespace is consistent.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const { t } = useTranslation();
|
|
14
|
+
* <Button>{t("save")}</Button>
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export function useTranslation() {
|
|
19
|
+
const { t, i18n } = useI18nTranslation(FIRECMS_NS);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Typed translation function scoped to FirecmsTranslations keys.
|
|
23
|
+
* Also supports i18next interpolation variables, e.g.
|
|
24
|
+
* t("add_to_field", { fieldName: "Tags" })
|
|
25
|
+
* t("error_deleting", { message: err.message })
|
|
26
|
+
*/
|
|
27
|
+
const typedT = (key: string, vars?: Record<string, string>): string =>
|
|
28
|
+
t(key, vars) as string;
|
|
29
|
+
|
|
30
|
+
return { t: typedT, i18n };
|
|
31
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import React, { PropsWithChildren, useEffect, useRef } from "react";
|
|
2
|
+
import i18next, { i18n } from "i18next";
|
|
3
|
+
import { I18nextProvider, initReactI18next } from "react-i18next";
|
|
4
|
+
import { en } from "../locales/en";
|
|
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
|
+
import { FireCMSTranslations } from "../types/translations";
|
|
12
|
+
|
|
13
|
+
const FIRECMS_NS = "firecms_core";
|
|
14
|
+
|
|
15
|
+
export const FIRECMS_LOCALE_STORAGE_KEY = "firecms_locale";
|
|
16
|
+
|
|
17
|
+
/** DeepPartial helper — allows partial overrides at any nesting level */
|
|
18
|
+
type DeepPartial<T> = T extends object
|
|
19
|
+
? { [K in keyof T]?: DeepPartial<T[K]> }
|
|
20
|
+
: T;
|
|
21
|
+
|
|
22
|
+
export interface FireCMSi18nProviderProps {
|
|
23
|
+
/** BCP-47 locale tag, e.g. "en", "es", "fr". Defaults to "en". */
|
|
24
|
+
locale?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Override or extend any FireCMS UI string, keyed by locale.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* translations={{
|
|
30
|
+
* en: { save: "Publish" },
|
|
31
|
+
* es: { save: "Publicar", discard: "Descartar" }
|
|
32
|
+
* }}
|
|
33
|
+
*/
|
|
34
|
+
translations?: {
|
|
35
|
+
[locale: string]: DeepPartial<FireCMSTranslations>;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initialises a dedicated i18next instance for FireCMS's internal UI strings.
|
|
41
|
+
*
|
|
42
|
+
* This instance is isolated from any app-level i18next configuration the
|
|
43
|
+
* consumer may have. Mount this at the top of the FireCMS component tree.
|
|
44
|
+
*
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
export function FireCMSi18nProvider({
|
|
48
|
+
locale = "en",
|
|
49
|
+
translations,
|
|
50
|
+
children
|
|
51
|
+
}: PropsWithChildren<FireCMSi18nProviderProps>) {
|
|
52
|
+
const i18nRef = useRef<i18n | null>(null);
|
|
53
|
+
const [ready, setReady] = React.useState(false);
|
|
54
|
+
|
|
55
|
+
if (!i18nRef.current) {
|
|
56
|
+
const instance = i18next.createInstance();
|
|
57
|
+
|
|
58
|
+
// Build the initial resources: English baseline + any consumer overrides
|
|
59
|
+
const resources = buildResources(translations);
|
|
60
|
+
|
|
61
|
+
let initialLocale = locale;
|
|
62
|
+
if (typeof window !== "undefined") {
|
|
63
|
+
const stored = localStorage.getItem(FIRECMS_LOCALE_STORAGE_KEY);
|
|
64
|
+
if (stored) initialLocale = stored;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
instance
|
|
68
|
+
.use(initReactI18next)
|
|
69
|
+
.init({
|
|
70
|
+
lng: initialLocale,
|
|
71
|
+
fallbackLng: "en",
|
|
72
|
+
ns: [FIRECMS_NS],
|
|
73
|
+
defaultNS: FIRECMS_NS,
|
|
74
|
+
resources,
|
|
75
|
+
interpolation: {
|
|
76
|
+
// React already escapes — don't double-escape
|
|
77
|
+
escapeValue: false,
|
|
78
|
+
},
|
|
79
|
+
}, () => {
|
|
80
|
+
setReady(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
instance.on("languageChanged", (lng) => {
|
|
84
|
+
if (typeof window !== "undefined") {
|
|
85
|
+
localStorage.setItem(FIRECMS_LOCALE_STORAGE_KEY, lng);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
i18nRef.current = instance;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// When `locale` prop changes, switch language on the existing instance
|
|
93
|
+
// ONLY if the user hasn't explicitly set a preference
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (i18nRef.current && i18nRef.current.language !== locale) {
|
|
96
|
+
const hasUserPreference = typeof window !== "undefined" && Boolean(localStorage.getItem(FIRECMS_LOCALE_STORAGE_KEY));
|
|
97
|
+
if (!hasUserPreference) {
|
|
98
|
+
i18nRef.current.changeLanguage(locale);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}, [locale]);
|
|
102
|
+
|
|
103
|
+
// When consumer translations prop changes, update the resource bundles
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!i18nRef.current) return;
|
|
106
|
+
const resources = buildResources(translations);
|
|
107
|
+
for (const [lang, bundle] of Object.entries(resources)) {
|
|
108
|
+
i18nRef.current.addResourceBundle(
|
|
109
|
+
lang,
|
|
110
|
+
FIRECMS_NS,
|
|
111
|
+
bundle[FIRECMS_NS],
|
|
112
|
+
true, // deep merge
|
|
113
|
+
true // overwrite existing keys
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}, [translations]);
|
|
117
|
+
|
|
118
|
+
if (!ready || !i18nRef.current) return null;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<I18nextProvider i18n={i18nRef.current}>
|
|
122
|
+
{children}
|
|
123
|
+
</I18nextProvider>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Build an i18next resources object from the English baseline plus any
|
|
129
|
+
* consumer-provided overrides.
|
|
130
|
+
*/
|
|
131
|
+
function buildResources(
|
|
132
|
+
translations?: { [locale: string]: DeepPartial<FireCMSTranslations> }
|
|
133
|
+
): Record<string, Record<string, object>> {
|
|
134
|
+
const resources: Record<string, Record<string, object>> = {
|
|
135
|
+
en: { [FIRECMS_NS]: { ...en } },
|
|
136
|
+
es: { [FIRECMS_NS]: { ...es } },
|
|
137
|
+
de: { [FIRECMS_NS]: { ...de } },
|
|
138
|
+
fr: { [FIRECMS_NS]: { ...fr } },
|
|
139
|
+
it: { [FIRECMS_NS]: { ...it } },
|
|
140
|
+
hi: { [FIRECMS_NS]: { ...hi } },
|
|
141
|
+
pt: { [FIRECMS_NS]: { ...pt } },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (!translations) return resources;
|
|
145
|
+
|
|
146
|
+
for (const [lang, overrides] of Object.entries(translations)) {
|
|
147
|
+
if (!resources[lang]) {
|
|
148
|
+
// For non-English/Spanish locales, start from English as the fallback base
|
|
149
|
+
resources[lang] = { [FIRECMS_NS]: { ...en } };
|
|
150
|
+
}
|
|
151
|
+
// Merge consumer overrides (shallow merge is enough since translations
|
|
152
|
+
// is a flat record — deepMerge option in addResourceBundle handles deeper)
|
|
153
|
+
resources[lang][FIRECMS_NS] = {
|
|
154
|
+
...resources[lang][FIRECMS_NS],
|
|
155
|
+
...overrides,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return resources;
|
|
160
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -99,8 +99,8 @@ function getNestedPropertiesDepth(property: ResolvedProperty, accumulator: numbe
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
export const useBuildSideEntityController = (navigation: NavigationController,
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
sideDialogsController: SideDialogsController,
|
|
103
|
+
authController: AuthController
|
|
104
104
|
): SideEntityController => {
|
|
105
105
|
|
|
106
106
|
const location = useLocation();
|
|
@@ -121,9 +121,9 @@ export const useBuildSideEntityController = (navigation: NavigationController,
|
|
|
121
121
|
for (let i = 0; i < panelsFromUrl.length; i++) {
|
|
122
122
|
const props = panelsFromUrl[i];
|
|
123
123
|
if (i === 0)
|
|
124
|
-
sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController));
|
|
124
|
+
sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search));
|
|
125
125
|
else
|
|
126
|
-
sideDialogsController.open(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController))
|
|
126
|
+
sideDialogsController.open(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search))
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
initialised.current = true;
|
|
@@ -143,7 +143,7 @@ export const useBuildSideEntityController = (navigation: NavigationController,
|
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
145
|
const lastPanel = panelsFromUrl[panelsFromUrl.length - 1];
|
|
146
|
-
const panelProps = propsToSidePanel(lastPanel, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController);
|
|
146
|
+
const panelProps = propsToSidePanel(lastPanel, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search);
|
|
147
147
|
const lastCurrentPanel = currentPanelKeys.length > 0 ? currentPanelKeys[currentPanelKeys.length - 1] : undefined;
|
|
148
148
|
if (!lastCurrentPanel || lastCurrentPanel !== panelProps.key) {
|
|
149
149
|
sideDialogsController.replace(panelProps);
|
|
@@ -156,7 +156,7 @@ export const useBuildSideEntityController = (navigation: NavigationController,
|
|
|
156
156
|
useEffect(() => {
|
|
157
157
|
const updatedSidePanels = sideDialogsController.sidePanels.map(sidePanelProps => {
|
|
158
158
|
if (sidePanelProps.additional) {
|
|
159
|
-
return propsToSidePanel(sidePanelProps.additional, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController);
|
|
159
|
+
return propsToSidePanel(sidePanelProps.additional, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search);
|
|
160
160
|
}
|
|
161
161
|
return sidePanelProps;
|
|
162
162
|
});
|
|
@@ -183,17 +183,18 @@ export const useBuildSideEntityController = (navigation: NavigationController,
|
|
|
183
183
|
|
|
184
184
|
sideDialogsController.open(
|
|
185
185
|
propsToSidePanel({
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
selectedTab: defaultSelectedView,
|
|
187
|
+
...props
|
|
188
|
+
},
|
|
189
189
|
navigation.buildUrlCollectionPath,
|
|
190
190
|
navigation.resolveIdsFrom,
|
|
191
191
|
smallLayout,
|
|
192
192
|
customizationController,
|
|
193
|
-
authController
|
|
193
|
+
authController,
|
|
194
|
+
location.search
|
|
194
195
|
));
|
|
195
196
|
|
|
196
|
-
}, [sideDialogsController, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, authController.user]);
|
|
197
|
+
}, [sideDialogsController, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, authController.user, location.search]);
|
|
197
198
|
|
|
198
199
|
const replace = useCallback((props: EntitySidePanelProps<any>) => {
|
|
199
200
|
|
|
@@ -201,9 +202,9 @@ export const useBuildSideEntityController = (navigation: NavigationController,
|
|
|
201
202
|
throw Error("If you want to copy an entity you need to provide an entityId");
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController));
|
|
205
|
+
sideDialogsController.replace(propsToSidePanel(props, navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, smallLayout, customizationController, authController, location.search));
|
|
205
206
|
|
|
206
|
-
}, [navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, sideDialogsController, smallLayout, authController.user]);
|
|
207
|
+
}, [navigation.buildUrlCollectionPath, navigation.resolveIdsFrom, sideDialogsController, smallLayout, authController.user, location.search]);
|
|
207
208
|
|
|
208
209
|
return {
|
|
209
210
|
close,
|
|
@@ -265,18 +266,19 @@ export function buildSidePanelsFromUrl(path: string, collections: EntityCollecti
|
|
|
265
266
|
}
|
|
266
267
|
|
|
267
268
|
const propsToSidePanel = (props: EntitySidePanelProps,
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
buildUrlCollectionPath: (path: string) => string,
|
|
270
|
+
resolveIdsFrom: (pathWithAliases: string) => string,
|
|
271
|
+
smallLayout: boolean,
|
|
272
|
+
customizationController: CustomizationController,
|
|
273
|
+
authController: AuthController,
|
|
274
|
+
locationSearch: string
|
|
273
275
|
): SideDialogPanelProps => {
|
|
274
276
|
|
|
275
277
|
const collectionPath = removeInitialAndTrailingSlashes(props.path);
|
|
276
278
|
|
|
277
279
|
const urlPath = props.entityId
|
|
278
|
-
? buildUrlCollectionPath(`${collectionPath}/${props.entityId}${props.selectedTab ? "/" + props.selectedTab : ""}#${SIDE_URL_HASH}`)
|
|
279
|
-
: buildUrlCollectionPath(`${collectionPath}#${NEW_URL_HASH}`);
|
|
280
|
+
? buildUrlCollectionPath(`${collectionPath}/${props.entityId}${props.selectedTab ? "/" + props.selectedTab : ""}${locationSearch}#${SIDE_URL_HASH}`)
|
|
281
|
+
: buildUrlCollectionPath(`${collectionPath}${locationSearch}#${NEW_URL_HASH}`);
|
|
280
282
|
|
|
281
283
|
const resolvedPanelProps: EntitySidePanelProps<any> = {
|
|
282
284
|
...props,
|