@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
@@ -0,0 +1,157 @@
1
+ import { EditorState, Transaction } from "prosemirror-state";
2
+ import { EditorBubbleItem } from "../components";
3
+ import { useTranslation } from "../../hooks/useTranslation";
4
+
5
+ import {
6
+ Button,
7
+ CheckBoxIcon,
8
+ CheckIcon,
9
+ CodeIcon,
10
+ FormatListBulletedIcon,
11
+ FormatListNumberedIcon,
12
+ FormatQuoteIcon,
13
+ KeyboardArrowDownIcon,
14
+ Looks3Icon,
15
+ LooksOneIcon,
16
+ LooksTwoIcon,
17
+ Popover,
18
+ TextFieldsIcon
19
+ } from "@firecms/ui";
20
+ import { useProseMirrorContext } from "../hooks/useProseMirrorContext";
21
+ import { isNodeActive } from "../utils/prosemirror-utils";
22
+ import { schema } from "../schema";
23
+ import { setBlockType, wrapIn } from "prosemirror-commands";
24
+ import { wrapInList } from "prosemirror-schema-list";
25
+
26
+ export type SelectorItem = {
27
+ name: string;
28
+ labelKey: string;
29
+ icon: React.ElementType;
30
+ command: (state: EditorState, dispatch: (tr: Transaction) => void) => void;
31
+ isActive: (state: EditorState) => boolean;
32
+ };
33
+
34
+ const items: SelectorItem[] = [
35
+ {
36
+ name: "Text",
37
+ labelKey: "editor_text",
38
+ icon: TextFieldsIcon,
39
+ command: (state, dispatch) => setBlockType(schema.nodes.paragraph)(state, dispatch),
40
+ isActive: (state) =>
41
+ isNodeActive(state, schema.nodes.paragraph) &&
42
+ !isNodeActive(state, schema.nodes.bullet_list) &&
43
+ !isNodeActive(state, schema.nodes.ordered_list),
44
+ },
45
+ {
46
+ name: "Heading 1",
47
+ labelKey: "editor_heading_1",
48
+ icon: LooksOneIcon,
49
+ command: (state, dispatch) => setBlockType(schema.nodes.heading, { level: 1 })(state, dispatch),
50
+ isActive: (state) => isNodeActive(state, schema.nodes.heading, { level: 1 }),
51
+ },
52
+ {
53
+ name: "Heading 2",
54
+ labelKey: "editor_heading_2",
55
+ icon: LooksTwoIcon,
56
+ command: (state, dispatch) => setBlockType(schema.nodes.heading, { level: 2 })(state, dispatch),
57
+ isActive: (state) => isNodeActive(state, schema.nodes.heading, { level: 2 }),
58
+ },
59
+ {
60
+ name: "Heading 3",
61
+ labelKey: "editor_heading_3",
62
+ icon: Looks3Icon,
63
+ command: (state, dispatch) => setBlockType(schema.nodes.heading, { level: 3 })(state, dispatch),
64
+ isActive: (state) => isNodeActive(state, schema.nodes.heading, { level: 3 }),
65
+ },
66
+ {
67
+ name: "To-do List",
68
+ labelKey: "editor_todo_list",
69
+ icon: CheckBoxIcon,
70
+ command: (state, dispatch) => wrapInList(schema.nodes.task_list)(state, dispatch),
71
+ isActive: (state) => isNodeActive(state, schema.nodes.task_item),
72
+ },
73
+ {
74
+ name: "Bullet List",
75
+ labelKey: "editor_bullet_list",
76
+ icon: FormatListBulletedIcon,
77
+ command: (state, dispatch) => wrapInList(schema.nodes.bullet_list)(state, dispatch),
78
+ isActive: (state) => isNodeActive(state, schema.nodes.bullet_list),
79
+ },
80
+ {
81
+ name: "Numbered List",
82
+ labelKey: "editor_numbered_list",
83
+ icon: FormatListNumberedIcon,
84
+ command: (state, dispatch) => wrapInList(schema.nodes.ordered_list)(state, dispatch),
85
+ isActive: (state) => isNodeActive(state, schema.nodes.ordered_list),
86
+ },
87
+ {
88
+ name: "Quote",
89
+ labelKey: "editor_quote",
90
+ icon: FormatQuoteIcon,
91
+ command: (state, dispatch) => wrapIn(schema.nodes.blockquote)(state, dispatch),
92
+ isActive: (state) => isNodeActive(state, schema.nodes.blockquote),
93
+ },
94
+ {
95
+ name: "Code",
96
+ labelKey: "editor_code",
97
+ icon: CodeIcon,
98
+ command: (state, dispatch) => setBlockType(schema.nodes.code_block)(state, dispatch),
99
+ isActive: (state) => isNodeActive(state, schema.nodes.code_block),
100
+ },
101
+ ];
102
+
103
+ interface NodeSelectorProps {
104
+ open: boolean;
105
+ onOpenChange: (open: boolean) => void;
106
+ portalContainer: HTMLElement | null;
107
+ }
108
+
109
+ export const NodeSelector = ({
110
+ open,
111
+ onOpenChange,
112
+ portalContainer
113
+ }: NodeSelectorProps) => {
114
+ const { state, view } = useProseMirrorContext();
115
+ const { t } = useTranslation();
116
+ if (!state || !view) return null;
117
+
118
+ const activeItem = items.filter((item) => item.isActive(state)).pop() ?? {
119
+ name: "Multiple",
120
+ labelKey: "editor_multiple",
121
+ };
122
+
123
+ return (
124
+ <Popover
125
+ sideOffset={5}
126
+ align="start"
127
+ portalContainer={portalContainer}
128
+ className="w-48 p-1"
129
+ trigger={<Button variant="text"
130
+ className="gap-2 rounded-none"
131
+ color="text">
132
+ <span className="whitespace-nowrap text-sm">{t(activeItem.labelKey)}</span>
133
+ <KeyboardArrowDownIcon size={"small"} />
134
+ </Button>}
135
+ modal={true}
136
+ open={open}
137
+ onOpenChange={onOpenChange}>
138
+ {items.map((item, index) => (
139
+ <EditorBubbleItem
140
+ key={index}
141
+ onSelect={() => {
142
+ item.command(view.state, view.dispatch);
143
+ view.focus();
144
+ onOpenChange(false);
145
+ }}
146
+ className="flex cursor-pointer items-center justify-between rounded px-2 py-1 text-sm hover:bg-blue-50 hover:dark:bg-surface-700 text-surface-900 dark:text-white"
147
+ >
148
+ <div className="flex items-center space-x-2">
149
+ <item.icon size="smallest" />
150
+ <span>{t(item.labelKey)}</span>
151
+ </div>
152
+ {activeItem.name === item.name && <CheckIcon size="smallest" />}
153
+ </EditorBubbleItem>
154
+ ))}
155
+ </Popover>
156
+ );
157
+ };
@@ -0,0 +1,86 @@
1
+ import { EditorState, Transaction } from "prosemirror-state";
2
+ import { EditorBubbleItem } from "../components";
3
+ import type { SelectorItem } from "./node-selector";
4
+ import {
5
+ Button,
6
+ cls,
7
+ CodeIcon,
8
+ FormatBoldIcon,
9
+ FormatItalicIcon,
10
+ FormatStrikethroughIcon,
11
+ FormatUnderlinedIcon
12
+ } from "@firecms/ui";
13
+ import { useProseMirrorContext } from "../hooks/useProseMirrorContext";
14
+ import { isMarkActive } from "../utils/prosemirror-utils";
15
+ import { schema } from "../schema";
16
+ import { toggleMark } from "prosemirror-commands";
17
+
18
+ export const TextButtons = () => {
19
+ const { state, view } = useProseMirrorContext();
20
+ if (!state || !view) return null;
21
+
22
+ // We pass state directly to isActive, and dispatch to command
23
+ const items = [
24
+ {
25
+ name: "bold",
26
+ labelKey: "editor_bold",
27
+ isActive: (s: EditorState) => isMarkActive(s, schema.marks.bold),
28
+ command: (s: EditorState, dispatch: (tr: Transaction) => void) => toggleMark(schema.marks.bold)(s, dispatch),
29
+ icon: FormatBoldIcon,
30
+ },
31
+ {
32
+ name: "italic",
33
+ labelKey: "editor_italic",
34
+ isActive: (s: EditorState) => isMarkActive(s, schema.marks.italic),
35
+ command: (s: EditorState, dispatch: (tr: Transaction) => void) => toggleMark(schema.marks.italic)(s, dispatch),
36
+ icon: FormatItalicIcon,
37
+ },
38
+ {
39
+ name: "underline",
40
+ labelKey: "editor_underline",
41
+ isActive: (s: EditorState) => isMarkActive(s, schema.marks.underline),
42
+ command: (s: EditorState, dispatch: (tr: Transaction) => void) => toggleMark(schema.marks.underline)(s, dispatch),
43
+ icon: FormatUnderlinedIcon,
44
+ },
45
+ {
46
+ name: "strike",
47
+ labelKey: "editor_strikethrough",
48
+ isActive: (s: EditorState) => isMarkActive(s, schema.marks.strike),
49
+ command: (s: EditorState, dispatch: (tr: Transaction) => void) => toggleMark(schema.marks.strike)(s, dispatch),
50
+ icon: FormatStrikethroughIcon,
51
+ },
52
+ {
53
+ name: "code",
54
+ labelKey: "editor_code",
55
+ isActive: (s: EditorState) => isMarkActive(s, schema.marks.code),
56
+ command: (s: EditorState, dispatch: (tr: Transaction) => void) => toggleMark(schema.marks.code)(s, dispatch),
57
+ icon: CodeIcon,
58
+ },
59
+ ];
60
+
61
+ return (
62
+ <div className="flex">
63
+ {items.map((item, index) => (
64
+ <EditorBubbleItem
65
+ key={index}
66
+ onSelect={() => {
67
+ item.command(view.state, view.dispatch);
68
+ view.focus();
69
+ }}
70
+ >
71
+ <Button size={"small"}
72
+ color="text"
73
+ className="gap-2 rounded-none h-full"
74
+ variant="text">
75
+ <item.icon
76
+ className={cls({
77
+ "text-inherit": !item.isActive(state),
78
+ "text-blue-500": item.isActive(state),
79
+ })}
80
+ />
81
+ </Button>
82
+ </EditorBubbleItem>
83
+ ))}
84
+ </div>
85
+ );
86
+ };
@@ -0,0 +1,6 @@
1
+ import { JSONContent } from "./components";
2
+ export type { JSONContent };
3
+
4
+ export type EditorAIController = {
5
+ autocomplete: (textBefore: string, textAfter: string, onUpdate: (delta: string) => void) => Promise<string>;
6
+ }
@@ -0,0 +1,126 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ import { EditorState, Transaction, Plugin } from "prosemirror-state";
3
+ import { EditorView } from "prosemirror-view";
4
+ import { schema } from "./schema";
5
+ import { markdownParser, markdownSerializer } from "./markdown";
6
+ import { removeClassesFromJson } from "./utils/remove_classes";
7
+ import { DOMParser as ProseMirrorDOMParser, DOMSerializer } from "prosemirror-model";
8
+
9
+ export interface UseProseMirrorOptions {
10
+ content?: any;
11
+ plugins?: Plugin[];
12
+ editable?: boolean;
13
+ onMarkdownContentChange?: (content: string) => void;
14
+ onJsonContentChange?: (content: any | null) => void;
15
+ onHtmlContentChange?: (content: string) => void;
16
+ version?: number;
17
+ }
18
+
19
+ export function useProseMirror(options: UseProseMirrorOptions) {
20
+ const mountRef = useRef<HTMLDivElement>(null);
21
+ const viewRef = useRef<EditorView | null>(null);
22
+ const [editorState, setEditorState] = useState<EditorState | null>(null);
23
+ const isUpdatingContentRef = useRef(false);
24
+
25
+ useEffect(() => {
26
+ if (!mountRef.current) return;
27
+
28
+ let doc;
29
+ try {
30
+ if (typeof options.content === "string") {
31
+ if (options.content.trim() === "") {
32
+ doc = schema.node("doc", null, [schema.node("paragraph")]);
33
+ } else if (options.content.startsWith("<")) {
34
+ const temp = document.createElement("div");
35
+ temp.innerHTML = options.content;
36
+ doc = ProseMirrorDOMParser.fromSchema(schema).parse(temp);
37
+ } else {
38
+ doc = markdownParser.parse(options.content);
39
+ }
40
+ } else if (options.content && typeof options.content === "object" && Object.keys(options.content).length > 0) {
41
+ doc = schema.nodeFromJSON(options.content);
42
+ } else {
43
+ doc = schema.node("doc", null, [schema.node("paragraph")]);
44
+ }
45
+ } catch (e) {
46
+ console.warn("Failed to parse initial content, falling back to empty doc", e);
47
+ doc = schema.node("doc", null, [schema.node("paragraph")]);
48
+ }
49
+
50
+ const state = EditorState.create({
51
+ doc,
52
+ schema,
53
+ plugins: options.plugins || [],
54
+ });
55
+
56
+ const view = new EditorView(mountRef.current, {
57
+ state,
58
+ editable: () => options.editable !== false,
59
+ dispatchTransaction(transaction: Transaction) {
60
+ const newState = view.state.apply(transaction);
61
+ view.updateState(newState);
62
+ setEditorState(newState);
63
+
64
+ if (transaction.docChanged && !isUpdatingContentRef.current) {
65
+
66
+ if (options.onMarkdownContentChange) {
67
+ options.onMarkdownContentChange(markdownSerializer.serialize(newState.doc));
68
+ }
69
+ if (options.onJsonContentChange) {
70
+ options.onJsonContentChange(removeClassesFromJson(newState.doc.toJSON()));
71
+ }
72
+ if (options.onHtmlContentChange) {
73
+ const div = document.createElement("div");
74
+ const fragment = DOMSerializer.fromSchema(schema).serializeFragment(newState.doc.content);
75
+ div.appendChild(fragment);
76
+ options.onHtmlContentChange(div.innerHTML);
77
+ }
78
+ }
79
+ },
80
+ });
81
+
82
+
83
+
84
+
85
+
86
+ viewRef.current = view;
87
+ setEditorState(state);
88
+
89
+ return () => {
90
+ view.destroy();
91
+ viewRef.current = null;
92
+ };
93
+ }, []); // Initialize once
94
+
95
+ // Handle external content updates (like version change)
96
+ useEffect(() => {
97
+ if (!viewRef.current || options.version === undefined || options.version <= 0) return;
98
+ try {
99
+ isUpdatingContentRef.current = true;
100
+ let doc;
101
+ if (typeof options.content === "string") {
102
+ doc = markdownParser.parse(options.content) || schema.node("doc", null, [schema.node("paragraph")]);
103
+ } else if (options.content) {
104
+ doc = schema.nodeFromJSON(options.content);
105
+ } else {
106
+ doc = schema.node("doc", null, [schema.node("paragraph")]);
107
+ }
108
+
109
+ const tr = viewRef.current.state.tr.replaceWith(0, viewRef.current.state.doc.content.size, doc);
110
+ viewRef.current.dispatch(tr);
111
+ isUpdatingContentRef.current = false;
112
+ } catch (e) {
113
+ console.error("Error updating content manually via version bump:", e);
114
+ isUpdatingContentRef.current = false;
115
+ }
116
+ }, [options.version]);
117
+
118
+ // Handle editable prop change
119
+ useEffect(() => {
120
+ if (viewRef.current && editorState) {
121
+ viewRef.current.setProps({ editable: () => options.editable !== false });
122
+ }
123
+ }, [options.editable, editorState]);
124
+
125
+ return { mountRef, view: viewRef.current, editorState };
126
+ }
@@ -0,0 +1,108 @@
1
+ import { EditorState } from "prosemirror-state";
2
+
3
+ export function isMarkActive(state: EditorState, type: any) {
4
+ if (!state || !type) return false;
5
+ const { from, $from, to, empty } = state.selection;
6
+ if (empty) return !!type.isInSet(state.storedMarks || $from.marks());
7
+ return state.doc.rangeHasMark(from, to, type);
8
+ }
9
+
10
+ export function isNodeActive(state: EditorState, type: any, attrs: any = {}) {
11
+ if (!state || !type) return false;
12
+ const { $from, to, node } = state.selection as any;
13
+ if (node) {
14
+ return node.type === type && (!attrs || Object.keys(attrs).every((key) => node.attrs[key] === attrs[key]));
15
+ }
16
+ return to <= $from.end() && $from.parent.type === type && (!attrs || Object.keys(attrs).every((key) => $from.parent.attrs[key] === attrs[key]));
17
+ }
18
+
19
+ export function getMarkAttributes(state: EditorState, type: any) {
20
+ if (!state || !type) return {};
21
+ const { from, $from, to, empty } = state.selection;
22
+ let mark;
23
+ if (empty) {
24
+ mark = type.isInSet(state.storedMarks || $from.marks());
25
+ } else {
26
+ let found = false;
27
+ state.doc.nodesBetween(from, to, (node) => {
28
+ if (found) return false;
29
+ const m = type.isInSet(node.marks);
30
+ if (m) {
31
+ mark = m;
32
+ found = true;
33
+ }
34
+ return true;
35
+ });
36
+ }
37
+ return mark ? mark.attrs : {};
38
+ }
39
+
40
+ export function setMark(type: any, attrs?: any) {
41
+ return (state: EditorState, dispatch?: (tr: any) => void) => {
42
+ const { empty, $cursor, ranges } = state.selection as any;
43
+ if ((empty && !$cursor) || !type) return false;
44
+ if (dispatch) {
45
+ if ($cursor) {
46
+ dispatch(state.tr.addStoredMark(type.create(attrs)));
47
+ } else {
48
+ let tr = state.tr;
49
+ for (let i = 0; i < ranges.length; i++) {
50
+ let { $from, $to } = ranges[i];
51
+ tr.addMark($from.pos, $to.pos, type.create(attrs));
52
+ }
53
+ dispatch(tr.scrollIntoView());
54
+ }
55
+ }
56
+ return true;
57
+ };
58
+ }
59
+
60
+ export function unsetMark(type: any) {
61
+ return (state: EditorState, dispatch?: (tr: any) => void) => {
62
+ const { empty, $cursor, ranges } = state.selection as any;
63
+ if ((empty && !$cursor) || !type) return false;
64
+ if (dispatch) {
65
+ let tr = state.tr;
66
+ if ($cursor) {
67
+ const parent = $cursor.parent;
68
+ let markStart = -1;
69
+ let markEnd = -1;
70
+ let currentMarkStart = -1;
71
+
72
+ parent.forEach((child: any, offset: number) => {
73
+ const childStart = $cursor.start() + offset;
74
+ const childEnd = childStart + child.nodeSize;
75
+
76
+ if (type.isInSet(child.marks)) {
77
+ if (currentMarkStart === -1) currentMarkStart = childStart;
78
+ if ($cursor.pos >= childStart && $cursor.pos <= childEnd) {
79
+ markStart = currentMarkStart;
80
+ }
81
+ } else {
82
+ if (currentMarkStart !== -1) {
83
+ if (markStart !== -1 && markEnd === -1) markEnd = childStart;
84
+ currentMarkStart = -1;
85
+ }
86
+ }
87
+ });
88
+
89
+ if (markStart !== -1 && markEnd === -1) {
90
+ markEnd = $cursor.end();
91
+ }
92
+
93
+ if (markStart !== -1 && markEnd !== -1) {
94
+ tr.removeMark(markStart, markEnd, type);
95
+ }
96
+ tr.removeStoredMark(type);
97
+ dispatch(tr.scrollIntoView());
98
+ } else {
99
+ for (let i = 0; i < ranges.length; i++) {
100
+ let { $from, $to } = ranges[i];
101
+ tr.removeMark($from.pos, $to.pos, type);
102
+ }
103
+ dispatch(tr.scrollIntoView());
104
+ }
105
+ }
106
+ return true;
107
+ };
108
+ }
@@ -0,0 +1,17 @@
1
+ export function removeClassesFromJson(jsonObj: any): any {
2
+ // If it's an array, apply the function to each element
3
+ if (Array.isArray(jsonObj)) {
4
+ return jsonObj.map(item => removeClassesFromJson(item));
5
+ } else if (typeof jsonObj === "object" && jsonObj !== null) { // If it's an object, recurse through its properties
6
+ // If the object has an `attrs` property and `class` field, delete the `class` field
7
+ if (jsonObj.attrs && typeof jsonObj.attrs === "object" && "class" in jsonObj.attrs) {
8
+ delete jsonObj.attrs.class;
9
+ }
10
+
11
+ // Apply the function recursively to object properties
12
+ Object.keys(jsonObj).forEach(key => {
13
+ jsonObj[key] = removeClassesFromJson(jsonObj[key]);
14
+ });
15
+ }
16
+ return jsonObj;
17
+ }
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+
3
+ export function useDebouncedCallback<T>(value: T, callback: () => void, immediate: boolean, timeoutMs = 300) {
4
+
5
+ const pendingUpdate = React.useRef(false);
6
+ const performUpdate = () => {
7
+ callback();
8
+ pendingUpdate.current = false;
9
+ };
10
+
11
+ const handlerRef = React.useRef<number | undefined>(undefined);
12
+
13
+ React.useEffect(
14
+ () => {
15
+ pendingUpdate.current = true;
16
+ clearTimeout(handlerRef.current);
17
+ handlerRef.current = setTimeout(performUpdate, timeoutMs) as any;
18
+ return () => {
19
+ if (immediate)
20
+ performUpdate();
21
+ };
22
+ },
23
+ [immediate, value]
24
+ );
25
+ }
@@ -40,7 +40,10 @@ import {
40
40
  useSnackbarController
41
41
  } from "../hooks";
42
42
  import { Alert, CheckIcon, Chip, cls, EditIcon, NotesIcon, paperMixin, Tooltip, Typography } from "@firecms/ui";
43
- import { Formex, FormexController, getIn, setIn, useCreateFormex } from "@firecms/formex";
43
+ import { Formex, FormexController, getIn, setIn, useCreateFormex,
44
+ useFormex
45
+ } from "@firecms/formex";
46
+ import { useTranslation } from "../hooks";
44
47
  import { useAnalyticsController } from "../hooks/useAnalyticsController";
45
48
  import { FormEntry, FormLayout, LabelWithIconAndTooltip, PropertyFieldBinding } from "../form";
46
49
  import { ValidationError } from "yup";
@@ -122,6 +125,35 @@ export function extractTouchedValues(values: any, touched: Record<string, boolea
122
125
  return acc;
123
126
  }
124
127
 
128
+ /**
129
+ * Recursively removes empty plain objects `{}` and empty arrays `[]` from a value tree.
130
+ * This prevents ghost containers created by `setIn` intermediate path construction
131
+ * (e.g. `{ address: {} }` when only `address.city` was touched but value is undefined)
132
+ * from falsely triggering the unsaved local changes indicator.
133
+ */
134
+ function removeEmptyContainers(obj: any): any {
135
+ if (Array.isArray(obj)) {
136
+ const cleaned = obj.map(removeEmptyContainers);
137
+ // Keep arrays even if they contain only nulls/undefined — that's intentional data
138
+ return cleaned;
139
+ }
140
+ if (obj && typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype) {
141
+ const result: Record<string, any> = {};
142
+ for (const key of Object.keys(obj)) {
143
+ const cleaned = removeEmptyContainers(obj[key]);
144
+ // Skip empty plain objects
145
+ if (cleaned && typeof cleaned === "object" && !Array.isArray(cleaned)
146
+ && Object.getPrototypeOf(cleaned) === Object.prototype
147
+ && Object.keys(cleaned).length === 0) {
148
+ continue;
149
+ }
150
+ result[key] = cleaned;
151
+ }
152
+ return result;
153
+ }
154
+ return obj;
155
+ }
156
+
125
157
  export function getChanges<T extends object>(source: Partial<T>, comparison: Partial<T>): Partial<T> {
126
158
  const changes: Partial<T> = {};
127
159
 
@@ -212,6 +244,7 @@ export function EntityForm<M extends Record<string, any>>({
212
244
 
213
245
  const sideEntityController = useSideEntityController();
214
246
  const navigationController = useNavigationController();
247
+ const { t } = useTranslation();
215
248
 
216
249
  const navigateBack = useCallback(() => {
217
250
  if (openEntityMode === "side_panel") {
@@ -247,7 +280,7 @@ export function EntityForm<M extends Record<string, any>>({
247
280
  const context = useFireCMSContext();
248
281
  const analyticsController = useAnalyticsController();
249
282
 
250
- const [underlyingChanges] = useState<Partial<EntityValues<M>>>({});
283
+ const [underlyingChanges, setUnderlyingChanges] = useState<Partial<EntityValues<M>>>({});
251
284
 
252
285
  const [customIdLoading, setCustomIdLoading] = useState<boolean>(false);
253
286
 
@@ -328,7 +361,15 @@ export function EntityForm<M extends Record<string, any>>({
328
361
  return [initialValues, initialDirty];
329
362
  }, [autoApplyLocalChanges, localChangesDataRaw, baseInitialValues, initialDirtyValues]);
330
363
 
331
- const hasLocalChanges = !localChangesCleared && localChangesDataRaw && Object.keys(localChangesDataRaw).length > 0;
364
+ const hasLocalChanges = useMemo(() => {
365
+ if (localChangesCleared || !localChangesDataRaw || Object.keys(localChangesDataRaw).length === 0) {
366
+ return false;
367
+ }
368
+ // Compare cached values against entity values to check for real differences
369
+ const entityValues = entity?.values ?? {};
370
+ const realChanges = getChanges(localChangesDataRaw as Partial<M>, entityValues as Partial<M>);
371
+ return Object.keys(realChanges).length > 0;
372
+ }, [localChangesCleared, localChangesDataRaw, entity?.values]);
332
373
 
333
374
  const formex: FormexController<M> = formexProp ?? useCreateFormex<M>({
334
375
  initialValues: initialValues as M,
@@ -348,8 +389,10 @@ export function EntityForm<M extends Record<string, any>>({
348
389
  onValuesChangeDeferred: (values: M, controller: FormexController<M>) => {
349
390
  const key = (status === "new" || status === "copy") ? path + "#new" : path + "/" + entityId;
350
391
  if (controller.dirty) {
351
- const touchedValues = extractTouchedValues(values, controller.touched);
352
- saveEntityToCache(key, touchedValues);
392
+ const touchedValues = removeEmptyContainers(extractTouchedValues(values, controller.touched));
393
+ if (touchedValues && Object.keys(touchedValues).length > 0) {
394
+ saveEntityToCache(key, touchedValues);
395
+ }
353
396
  }
354
397
  },
355
398
  validation: (values) => {
@@ -366,6 +409,15 @@ export function EntityForm<M extends Record<string, any>>({
366
409
  useEffect(() => {
367
410
 
368
411
  const handleKeyDown = (e: KeyboardEvent) => {
412
+ if (e.defaultPrevented) return;
413
+ const activeElement = document.activeElement as HTMLElement;
414
+ const isInput = activeElement && (
415
+ activeElement.tagName === "INPUT" ||
416
+ activeElement.tagName === "TEXTAREA" ||
417
+ activeElement.isContentEditable
418
+ );
419
+ if (isInput) return;
420
+
369
421
  const isUndo = (e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z";
370
422
  const isRedo =
371
423
  ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "z") ||
@@ -653,6 +705,27 @@ export function EntityForm<M extends Record<string, any>>({
653
705
 
654
706
  useOnAutoSave(autoSave, formex, lastSavedValues, save);
655
707
 
708
+ // Detect external changes to the entity (e.g. from onSnapshot after Admin SDK writes)
709
+ const prevEntityValuesRef = useRef<EntityValues<M> | undefined>(entity?.values);
710
+ useEffect(() => {
711
+ if (!entity?.values || status !== "existing") return;
712
+ const prev = prevEntityValuesRef.current;
713
+ prevEntityValuesRef.current = entity.values;
714
+ if (prev && !equal(prev, entity.values)) {
715
+ // Compute the diff between the old and new entity values
716
+ const changes: Partial<EntityValues<M>> = {};
717
+ const allKeys = new Set([...Object.keys(prev), ...Object.keys(entity.values)]);
718
+ for (const key of allKeys) {
719
+ if (!equal((prev as any)[key], (entity.values as any)[key])) {
720
+ (changes as any)[key] = (entity.values as any)[key];
721
+ }
722
+ }
723
+ if (Object.keys(changes).length > 0) {
724
+ setUnderlyingChanges(changes);
725
+ }
726
+ }
727
+ }, [entity?.values, status]);
728
+
656
729
  useEffect(() => {
657
730
  if (!autoSave && !formex.isSubmitting && underlyingChanges && entity) {
658
731
  // we update the form fields from the Firestore data
@@ -767,7 +840,7 @@ export function EntityForm<M extends Record<string, any>>({
767
840
 
768
841
  {!entity?.values && initialStatus === "existing" &&
769
842
  <Alert color={"warning"} size={"small"} outerClassName={"w-full mb-4 text-xs"}>
770
- This entity does not exist in the database
843
+ {t("this_entity_not_exist")}
771
844
  </Alert>}
772
845
 
773
846
  {showEntityPath && <Alert color={"base"} outerClassName={"w-full"} size={"small"}>
@@ -781,7 +854,7 @@ export function EntityForm<M extends Record<string, any>>({
781
854
  {children}
782
855
 
783
856
  {initialEntityId && !entity && initialStatus !== "new" && <Alert color={"info"} size={"small"}>
784
- This entity does not exist in the database
857
+ {t("this_entity_not_exist")}
785
858
  </Alert>}
786
859
 
787
860
  {!Builder && !collection.hideIdFromForm &&