@firecms/core 3.1.0-canary.24c8270 → 3.1.0-canary.501d471

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 (224) hide show
  1. package/dist/components/EntityCollectionView/CollectionDataErrorBanner.d.ts +4 -0
  2. package/dist/components/ErrorBoundary.d.ts +3 -1
  3. package/dist/components/HomePage/DefaultHomePage.d.ts +0 -1
  4. package/dist/components/LanguageToggle.d.ts +1 -0
  5. package/dist/components/UnsavedChangesDialog.d.ts +1 -0
  6. package/dist/components/VirtualTable/VirtualTableHeader.d.ts +1 -0
  7. package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
  8. package/dist/components/VirtualTable/VirtualTableProps.d.ts +6 -1
  9. package/dist/components/VirtualTable/types.d.ts +1 -0
  10. package/dist/components/index.d.ts +1 -0
  11. package/dist/core/DrawerNavigationGroup.d.ts +2 -2
  12. package/dist/editor/components/SlashCommandMenu.d.ts +6 -0
  13. package/dist/editor/components/editor-bubble-item.d.ts +8 -0
  14. package/dist/editor/components/editor-bubble.d.ts +8 -0
  15. package/dist/editor/components/image-bubble.d.ts +5 -0
  16. package/dist/editor/components/index.d.ts +16 -0
  17. package/dist/editor/components/table-bubble.d.ts +5 -0
  18. package/dist/editor/editor.d.ts +30 -0
  19. package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
  20. package/dist/editor/extensions/Image/index.d.ts +6 -0
  21. package/dist/editor/extensions/Image.d.ts +6 -0
  22. package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
  23. package/dist/editor/extensions/clipboard.d.ts +7 -0
  24. package/dist/editor/extensions/custom-keymap.d.ts +1 -0
  25. package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
  26. package/dist/editor/hooks/useProseMirror.d.ts +13 -0
  27. package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
  28. package/dist/editor/index.d.ts +2 -0
  29. package/dist/editor/markdown.d.ts +5 -0
  30. package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
  31. package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
  32. package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
  33. package/dist/editor/nodeViews/index.d.ts +6 -0
  34. package/dist/editor/plugins/index.d.ts +2 -0
  35. package/dist/editor/plugins/inputrules.d.ts +6 -0
  36. package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
  37. package/dist/editor/plugins/slashCommandPlugin.d.ts +12 -0
  38. package/dist/editor/schema.d.ts +2 -0
  39. package/dist/editor/selectors/ai-selector.d.ts +0 -0
  40. package/dist/editor/selectors/color-selector.d.ts +10 -0
  41. package/dist/editor/selectors/link-selector.d.ts +8 -0
  42. package/dist/editor/selectors/node-selector.d.ts +15 -0
  43. package/dist/editor/selectors/text-buttons.d.ts +1 -0
  44. package/dist/editor/types.d.ts +5 -0
  45. package/dist/editor/useProseMirror.d.ts +16 -0
  46. package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
  47. package/dist/editor/utils/remove_classes.d.ts +1 -0
  48. package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
  49. package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
  50. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  51. package/dist/hooks/index.d.ts +1 -0
  52. package/dist/hooks/useBuildNavigationController.d.ts +0 -1
  53. package/dist/hooks/useCollapsedGroups.d.ts +3 -3
  54. package/dist/hooks/useTranslation.d.ts +17 -0
  55. package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
  56. package/dist/index.d.ts +5 -0
  57. package/dist/index.es.js +29889 -18645
  58. package/dist/index.es.js.map +1 -1
  59. package/dist/index.umd.js +29883 -18659
  60. package/dist/index.umd.js.map +1 -1
  61. package/dist/locales/de.d.ts +2 -0
  62. package/dist/locales/en.d.ts +10 -0
  63. package/dist/locales/es.d.ts +10 -0
  64. package/dist/locales/fr.d.ts +2 -0
  65. package/dist/locales/hi.d.ts +2 -0
  66. package/dist/locales/it.d.ts +2 -0
  67. package/dist/locales/pt.d.ts +7 -0
  68. package/dist/types/collections.d.ts +38 -0
  69. package/dist/types/customization_controller.d.ts +2 -1
  70. package/dist/types/firecms.d.ts +2 -1
  71. package/dist/types/index.d.ts +1 -0
  72. package/dist/types/navigation.d.ts +2 -2
  73. package/dist/types/plugins.d.ts +7 -0
  74. package/dist/types/properties.d.ts +9 -8
  75. package/dist/types/storage.d.ts +1 -0
  76. package/dist/types/translations.d.ts +669 -0
  77. package/dist/util/index.d.ts +1 -0
  78. package/dist/util/lazy_eager.d.ts +7 -0
  79. package/dist/util/objects.d.ts +1 -0
  80. package/dist/util/useStorageUploadController.d.ts +10 -1
  81. package/package.json +45 -9
  82. package/src/app/Scaffold.tsx +7 -5
  83. package/src/components/AIIcon.tsx +3 -1
  84. package/src/components/ArrayContainer.tsx +6 -4
  85. package/src/components/ClearFilterSortButton.tsx +6 -3
  86. package/src/components/ConfirmationDialog.tsx +4 -2
  87. package/src/components/DeleteEntityDialog.tsx +10 -7
  88. package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +9 -3
  89. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
  90. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
  91. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
  92. package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
  93. package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
  94. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +16 -43
  95. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
  96. package/src/components/EntityCollectionView/EntityCollectionView.tsx +24 -18
  97. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
  98. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
  99. package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
  100. package/src/components/EntityCollectionView/ViewModeToggle.tsx +11 -8
  101. package/src/components/EntityJsonPreview.tsx +2 -1
  102. package/src/components/EntityView.tsx +3 -2
  103. package/src/components/ErrorBoundary.tsx +27 -15
  104. package/src/components/HomePage/DefaultHomePage.tsx +19 -13
  105. package/src/components/HomePage/HomePageDnD.tsx +3 -1
  106. package/src/components/HomePage/NavigationGroup.tsx +3 -1
  107. package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
  108. package/src/components/LanguageToggle.tsx +66 -0
  109. package/src/components/NotFoundPage.tsx +5 -3
  110. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
  111. package/src/components/ReferenceWidget.tsx +3 -2
  112. package/src/components/SearchIconsView.tsx +3 -1
  113. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
  114. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
  115. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
  116. package/src/components/UnsavedChangesDialog.tsx +6 -4
  117. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
  118. package/src/components/VirtualTable/VirtualTable.tsx +5 -3
  119. package/src/components/VirtualTable/VirtualTableHeader.tsx +21 -18
  120. package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +8 -3
  121. package/src/components/VirtualTable/VirtualTableProps.tsx +7 -1
  122. package/src/components/VirtualTable/types.tsx +1 -0
  123. package/src/components/common/default_entity_actions.tsx +4 -0
  124. package/src/components/common/useDataSourceTableController.tsx +5 -14
  125. package/src/components/index.tsx +1 -0
  126. package/src/core/DefaultAppBar.tsx +14 -10
  127. package/src/core/DefaultDrawer.tsx +8 -2
  128. package/src/core/DrawerNavigationGroup.tsx +5 -3
  129. package/src/core/EntityEditView.tsx +53 -7
  130. package/src/core/EntityEditViewFormActions.tsx +24 -17
  131. package/src/core/EntitySidePanel.tsx +6 -4
  132. package/src/core/FireCMS.tsx +33 -6
  133. package/src/core/field_configs.tsx +4 -2
  134. package/src/editor/components/SlashCommandMenu.tsx +516 -0
  135. package/src/editor/components/editor-bubble-item.tsx +32 -0
  136. package/src/editor/components/editor-bubble.tsx +118 -0
  137. package/src/editor/components/image-bubble.tsx +156 -0
  138. package/src/editor/components/index.ts +14 -0
  139. package/src/editor/components/table-bubble.tsx +165 -0
  140. package/src/editor/editor.tsx +455 -0
  141. package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
  142. package/src/editor/extensions/Image/index.ts +133 -0
  143. package/src/editor/extensions/Image.ts +159 -0
  144. package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
  145. package/src/editor/extensions/clipboard.ts +72 -0
  146. package/src/editor/extensions/custom-keymap.ts +24 -0
  147. package/src/editor/extensions/drag-and-drop.tsx +480 -0
  148. package/src/editor/hooks/useProseMirror.ts +124 -0
  149. package/src/editor/hooks/useProseMirrorContext.ts +15 -0
  150. package/src/editor/index.ts +2 -0
  151. package/src/editor/markdown.ts +172 -0
  152. package/src/editor/nodeViews/ImageComponent.tsx +20 -0
  153. package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
  154. package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
  155. package/src/editor/nodeViews/index.ts +35 -0
  156. package/src/editor/plugins/index.ts +58 -0
  157. package/src/editor/plugins/inputrules.ts +82 -0
  158. package/src/editor/plugins/placeholderPlugin.ts +55 -0
  159. package/src/editor/plugins/slashCommandPlugin.ts +61 -0
  160. package/src/editor/schema.ts +240 -0
  161. package/src/editor/selectors/ai-selector.tsx +111 -0
  162. package/src/editor/selectors/color-selector.tsx +200 -0
  163. package/src/editor/selectors/link-selector.tsx +118 -0
  164. package/src/editor/selectors/node-selector.tsx +157 -0
  165. package/src/editor/selectors/text-buttons.tsx +86 -0
  166. package/src/editor/types.ts +6 -0
  167. package/src/editor/useProseMirror.ts +126 -0
  168. package/src/editor/utils/prosemirror-utils.ts +108 -0
  169. package/src/editor/utils/remove_classes.ts +17 -0
  170. package/src/editor/utils/useDebouncedCallback.ts +25 -0
  171. package/src/form/EntityForm.tsx +80 -7
  172. package/src/form/EntityFormActions.tsx +19 -12
  173. package/src/form/PropertyFieldBinding.tsx +7 -5
  174. package/src/form/components/LocalChangesMenu.tsx +13 -13
  175. package/src/form/components/StorageItemPreview.tsx +3 -2
  176. package/src/form/components/StorageUploadProgress.tsx +18 -3
  177. package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +18 -5
  178. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +22 -9
  179. package/src/form/field_bindings/BlockFieldBinding.tsx +26 -9
  180. package/src/form/field_bindings/DateTimeFieldBinding.tsx +1 -1
  181. package/src/form/field_bindings/KeyValueFieldBinding.tsx +46 -24
  182. package/src/form/field_bindings/MapFieldBinding.tsx +27 -11
  183. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +73 -36
  184. package/src/form/field_bindings/MultiSelectFieldBinding.tsx +15 -1
  185. package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +25 -11
  186. package/src/form/field_bindings/ReferenceFieldBinding.tsx +25 -11
  187. package/src/form/field_bindings/RepeatFieldBinding.tsx +21 -6
  188. package/src/form/field_bindings/SelectFieldBinding.tsx +7 -5
  189. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +28 -10
  190. package/src/form/field_bindings/SwitchFieldBinding.tsx +31 -14
  191. package/src/form/field_bindings/TextFieldBinding.tsx +10 -7
  192. package/src/form/field_bindings/UserSelectFieldBinding.tsx +7 -5
  193. package/src/hooks/index.tsx +1 -0
  194. package/src/hooks/useBuildNavigationController.tsx +20 -13
  195. package/src/hooks/useCollapsedGroups.ts +7 -6
  196. package/src/hooks/useTranslation.ts +31 -0
  197. package/src/i18n/FireCMSi18nProvider.tsx +160 -0
  198. package/src/index.ts +5 -0
  199. package/src/locales/de.ts +718 -0
  200. package/src/locales/en.ts +730 -0
  201. package/src/locales/es.ts +730 -0
  202. package/src/locales/fr.ts +718 -0
  203. package/src/locales/hi.ts +718 -0
  204. package/src/locales/it.ts +718 -0
  205. package/src/locales/pt.ts +727 -0
  206. package/src/preview/PropertyPreview.tsx +3 -2
  207. package/src/preview/components/ReferencePreview.tsx +2 -1
  208. package/src/preview/components/UrlComponentPreview.tsx +4 -2
  209. package/src/preview/components/UserPreview.tsx +3 -1
  210. package/src/preview/property_previews/MapPropertyPreview.tsx +49 -27
  211. package/src/routes/FireCMSRoute.tsx +63 -54
  212. package/src/types/collections.ts +40 -0
  213. package/src/types/customization_controller.tsx +2 -1
  214. package/src/types/firecms.tsx +2 -1
  215. package/src/types/index.ts +1 -0
  216. package/src/types/navigation.ts +2 -2
  217. package/src/types/plugins.tsx +8 -0
  218. package/src/types/properties.ts +12 -10
  219. package/src/types/storage.ts +2 -1
  220. package/src/types/translations.ts +752 -0
  221. package/src/util/index.ts +1 -0
  222. package/src/util/lazy_eager.tsx +33 -0
  223. package/src/util/objects.ts +15 -0
  224. package/src/util/useStorageUploadController.tsx +23 -29
@@ -38,9 +38,12 @@ import {
38
38
  fieldBackgroundDisabledMixin,
39
39
  fieldBackgroundHoverMixin,
40
40
  fieldBackgroundMixin,
41
- Typography
41
+ Typography,
42
+ IconButton,
43
+ CloseIcon
42
44
  } from "@firecms/ui";
43
45
  import { useClearRestoreValue } from "../useClearRestoreValue";
46
+ import { useTranslation } from "../../hooks/useTranslation";
44
47
 
45
48
  const dropZoneClasses = "box-border relative pt-[2px] items-center border border-transparent min-h-[254px] outline-none rounded-md duration-200 ease-[cubic-bezier(0.4,0,0.2,1)] focus:border-primary-solid";
46
49
  const disabledClasses = fieldBackgroundDisabledMixin;
@@ -109,12 +112,27 @@ export function StorageUploadFieldBinding({
109
112
  <>
110
113
 
111
114
  {!minimalistView &&
112
- <LabelWithIconAndTooltip
113
- propertyKey={propertyKey}
114
- icon={getIconForProperty(property, "small")}
115
- required={property.validation?.required}
116
- title={property.name}
117
- className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"} />}
115
+ <div className="flex items-center w-full">
116
+ <LabelWithIconAndTooltip
117
+ propertyKey={propertyKey}
118
+ icon={getIconForProperty(property, "small")}
119
+ required={property.validation?.required}
120
+ title={property.name}
121
+ className={"h-8 text-text-secondary dark:text-text-secondary-dark ml-3.5"} />
122
+ <div className="flex-grow"/>
123
+ {(property.nullable || property.clearable) && !disabled && (
124
+ <IconButton
125
+ size="small"
126
+ onClick={(e) => {
127
+ e.stopPropagation();
128
+ e.preventDefault();
129
+ setValue(null);
130
+ }}
131
+ >
132
+ <CloseIcon size={"small"}/>
133
+ </IconButton>
134
+ )}
135
+ </div>}
118
136
 
119
137
  <StorageUpload
120
138
  value={internalValue}
@@ -135,7 +153,6 @@ export function StorageUploadFieldBinding({
135
153
  error={error}
136
154
  disabled={disabled}
137
155
  property={property} />
138
-
139
156
  </>
140
157
  );
141
158
  }
@@ -387,6 +404,7 @@ export function StorageUpload({
387
404
  storage,
388
405
  storagePathBuilder,
389
406
  }: StorageUploadProps) {
407
+ const { t } = useTranslation();
390
408
 
391
409
  if (multipleFilesSupported) {
392
410
  const arrayProperty = property as ResolvedArrayProperty<string[]>;
@@ -461,8 +479,8 @@ export function StorageUpload({
461
479
  }, [value, multipleFilesSupported, onChange, setInternalValue]);
462
480
 
463
481
  const helpText = multipleFilesSupported
464
- ? "Drag 'n' drop some files here, or click to select files. Drag to reorder."
465
- : "Drag 'n' drop a file here, or click to select one";
482
+ ? t("drag_drop_multiple")
483
+ : t("drag_drop_single");
466
484
 
467
485
  const renderProperty: ResolvedStringProperty = multipleFilesSupported
468
486
  ? (property as ResolvedArrayProperty<string[]>).of as ResolvedStringProperty
@@ -3,7 +3,7 @@ import React from "react";
3
3
  import { FieldProps } from "../../types";
4
4
  import { getIconForProperty } from "../../util";
5
5
  import { FieldHelperText, LabelWithIcon } from "../components";
6
- import { BooleanSwitchWithLabel } from "@firecms/ui";
6
+ import { BooleanSwitchWithLabel, IconButton, CloseIcon } from "@firecms/ui";
7
7
  import { useClearRestoreValue } from "../useClearRestoreValue";
8
8
  import { PropertyIdCopyTooltip } from "../../components";
9
9
 
@@ -39,19 +39,36 @@ export const SwitchFieldBinding = function SwitchFieldBinding({
39
39
  <>
40
40
 
41
41
  <PropertyIdCopyTooltip propertyKey={propertyKey}>
42
- <BooleanSwitchWithLabel
43
- value={value}
44
- onValueChange={(v) => setValue(v)}
45
- error={showError}
46
- className={property.widthPercentage !== undefined ? "mt-8" : undefined}
47
- label={<LabelWithIcon
48
- icon={getIconForProperty(property, "small")}
49
- required={property.validation?.required}
50
- title={property.name}/>}
51
- disabled={disabled}
52
- autoFocus={autoFocus}
53
- size={size}
54
- />
42
+ <div className="flex items-center">
43
+ <BooleanSwitchWithLabel
44
+ value={value}
45
+ onValueChange={(v) => setValue(v)}
46
+ error={showError}
47
+ className={property.widthPercentage !== undefined ? "mt-8" : undefined}
48
+ label={<LabelWithIcon
49
+ icon={getIconForProperty(property, "small")}
50
+ required={property.validation?.required}
51
+ title={property.name}/>}
52
+ disabled={disabled}
53
+ autoFocus={autoFocus}
54
+ size={size}
55
+ switchAdornment={
56
+ (property.nullable || property.clearable) && !disabled && value !== null && (
57
+ <IconButton
58
+ size="small"
59
+ onClick={(e) => {
60
+ e.stopPropagation();
61
+ e.preventDefault();
62
+ setValue(null);
63
+ }}
64
+ className="mr-2"
65
+ >
66
+ <CloseIcon size={"small"}/>
67
+ </IconButton>
68
+ )
69
+ }
70
+ />
71
+ </div>
55
72
  </PropertyIdCopyTooltip>
56
73
 
57
74
  <FieldHelperText includeDescription={includeDescription}
@@ -100,10 +100,10 @@ export function TextFieldBinding<T extends string | number>({
100
100
  showError && error ? "text-red-500 dark:text-red-600" : ""
101
101
  )}
102
102
  />
103
- {property.clearable && (
103
+ {(property.nullable || property.clearable) && value !== null && value !== undefined && (
104
104
  <div className="flex flex-row justify-center items-center absolute h-full right-0 top-0 mr-4">
105
- <IconButton onClick={handleClearClick}>
106
- <CloseIcon />
105
+ <IconButton size="small" onClick={handleClearClick}>
106
+ <CloseIcon size="small" />
107
107
  </IconButton>
108
108
  </div>
109
109
  )}
@@ -119,10 +119,13 @@ export function TextFieldBinding<T extends string | number>({
119
119
  type={inputType}
120
120
  disabled={disabled}
121
121
  endAdornment={
122
- property.clearable && <IconButton
123
- onClick={handleClearClick}>
124
- <CloseIcon />
125
- </IconButton>
122
+ (property.nullable || property.clearable) && value !== null && value !== undefined ? (
123
+ <IconButton
124
+ size="small"
125
+ onClick={handleClearClick}>
126
+ <CloseIcon size="small" />
127
+ </IconButton>
128
+ ) : undefined
126
129
  }
127
130
  error={showError ? error : undefined}
128
131
  inputClassName={error ? "text-red-500 dark:text-red-600" : ""} />
@@ -59,11 +59,13 @@ export function UserSelectFieldBinding({
59
59
  />
60
60
  </PropertyIdCopyTooltip>}
61
61
  endAdornment={
62
- property.clearable && !disabled && value && <IconButton
63
- size="small"
64
- onClick={handleClearClick}>
65
- <CloseIcon size={"small"}/>
66
- </IconButton>
62
+ (property.nullable || property.clearable) && !disabled && value !== null && value !== undefined ? (
63
+ <IconButton
64
+ size="small"
65
+ onClick={handleClearClick}>
66
+ <CloseIcon size={"small"}/>
67
+ </IconButton>
68
+ ) : undefined
67
69
  }
68
70
  onValueChange={(updatedValue: string) => {
69
71
  const newValue = updatedValue || null;
@@ -31,3 +31,4 @@ export * from "./useBuildLocalConfigurationPersistence";
31
31
  export * from "./useBuildModeController";
32
32
 
33
33
  export * from "./useValidateAuthenticator";
34
+ export * from "./useTranslation";
@@ -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 ?? NAVIGATION_DEFAULT_GROUP_NAME
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 ?? NAVIGATION_DEFAULT_GROUP_NAME
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(Boolean) as string[];
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
- const uniqueGroups = [...nonAdminGroups, ...adminGroups];
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,
@@ -682,12 +689,12 @@ async function resolveCMSViews(
682
689
  return resolvedViews;
683
690
  }
684
691
 
685
- function getGroup(collectionOrView: EntityCollection<any, any> | CMSView) {
692
+ function getGroup(collectionOrView: EntityCollection<any, any> | CMSView): string | null {
686
693
  const trimmed = collectionOrView.group?.trim();
687
694
  if (!trimmed || trimmed === "") {
688
- return NAVIGATION_DEFAULT_GROUP_NAME;
695
+ return null;
689
696
  }
690
- return trimmed ?? NAVIGATION_DEFAULT_GROUP_NAME;
697
+ return trimmed;
691
698
  }
692
699
 
693
700
  function areCollectionListsEqual(a: EntityCollection[], b: EntityCollection[]) {
@@ -823,7 +830,7 @@ function computeNavigationGroups({
823
830
  (collections ?? []).forEach(collection => {
824
831
  const entry = collection.id ?? collection.path;
825
832
  if (!assignedEntries.has(entry)) {
826
- const groupName = getGroup(collection);
833
+ const groupName = getGroup(collection) ?? "__default__";
827
834
  if (!unassignedGroupMap[groupName]) unassignedGroupMap[groupName] = [];
828
835
  unassignedGroupMap[groupName].push(entry);
829
836
  }
@@ -833,7 +840,7 @@ function computeNavigationGroups({
833
840
  (views ?? []).forEach(view => {
834
841
  const entry = view.path;
835
842
  if (!assignedEntries.has(entry)) {
836
- const groupName = getGroup(view);
843
+ const groupName = getGroup(view) ?? "__default__";
837
844
  if (!unassignedGroupMap[groupName]) unassignedGroupMap[groupName] = [];
838
845
  unassignedGroupMap[groupName].push(entry);
839
846
  }
@@ -861,7 +868,7 @@ function computeNavigationGroups({
861
868
 
862
869
  // Add collections
863
870
  (collections ?? []).forEach(collection => {
864
- const name = getGroup(collection);
871
+ const name = getGroup(collection) ?? "__default__";
865
872
  const entry = collection.id ?? collection.path;
866
873
  if (!groupMap[name]) groupMap[name] = [];
867
874
  groupMap[name].push(entry);
@@ -869,7 +876,7 @@ function computeNavigationGroups({
869
876
 
870
877
  // Add views
871
878
  (views ?? []).forEach(view => {
872
- const name = getGroup(view);
879
+ const name = getGroup(view) ?? "__default__";
873
880
  const entry = view.path;
874
881
  if (!groupMap[name]) groupMap[name] = [];
875
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: string) => {
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: string) => {
64
- setCollapsedGroups(prev => ({ ...prev, [name]: !prev[name] }));
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
@@ -7,3 +7,8 @@ export * from "./hooks";
7
7
  export * from "./components";
8
8
  export * from "./util";
9
9
  export * from "./contexts";
10
+ export * from "./i18n/FireCMSi18nProvider";
11
+ export * from "./locales/en";
12
+ export * from "./locales/es";
13
+ export * from "./editor";
14
+ export * from "./util/objects";