@firecms/core 3.1.0 → 3.2.0-canary.9c3d298

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 (191) 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/index.d.ts +1 -0
  7. package/dist/core/DrawerNavigationGroup.d.ts +2 -2
  8. package/dist/editor/components/SlashCommandMenu.d.ts +6 -0
  9. package/dist/editor/components/editor-bubble-item.d.ts +8 -0
  10. package/dist/editor/components/editor-bubble.d.ts +8 -0
  11. package/dist/editor/components/image-bubble.d.ts +5 -0
  12. package/dist/editor/components/index.d.ts +16 -0
  13. package/dist/editor/components/table-bubble.d.ts +5 -0
  14. package/dist/editor/editor.d.ts +30 -0
  15. package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
  16. package/dist/editor/extensions/Image/index.d.ts +6 -0
  17. package/dist/editor/extensions/Image.d.ts +6 -0
  18. package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
  19. package/dist/editor/extensions/clipboard.d.ts +7 -0
  20. package/dist/editor/extensions/custom-keymap.d.ts +1 -0
  21. package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
  22. package/dist/editor/hooks/useProseMirror.d.ts +13 -0
  23. package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
  24. package/dist/editor/index.d.ts +2 -0
  25. package/dist/editor/markdown.d.ts +5 -0
  26. package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
  27. package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
  28. package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
  29. package/dist/editor/nodeViews/index.d.ts +6 -0
  30. package/dist/editor/plugins/index.d.ts +2 -0
  31. package/dist/editor/plugins/inputrules.d.ts +6 -0
  32. package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
  33. package/dist/editor/plugins/slashCommandPlugin.d.ts +12 -0
  34. package/dist/editor/schema.d.ts +2 -0
  35. package/dist/editor/selectors/ai-selector.d.ts +0 -0
  36. package/dist/editor/selectors/color-selector.d.ts +10 -0
  37. package/dist/editor/selectors/link-selector.d.ts +8 -0
  38. package/dist/editor/selectors/node-selector.d.ts +15 -0
  39. package/dist/editor/selectors/text-buttons.d.ts +1 -0
  40. package/dist/editor/types.d.ts +5 -0
  41. package/dist/editor/useProseMirror.d.ts +16 -0
  42. package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
  43. package/dist/editor/utils/remove_classes.d.ts +1 -0
  44. package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
  45. package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
  46. package/dist/hooks/index.d.ts +1 -0
  47. package/dist/hooks/useBuildNavigationController.d.ts +0 -1
  48. package/dist/hooks/useCollapsedGroups.d.ts +3 -3
  49. package/dist/hooks/useTranslation.d.ts +17 -0
  50. package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
  51. package/dist/index.d.ts +4 -0
  52. package/dist/index.es.js +12898 -2265
  53. package/dist/index.es.js.map +1 -1
  54. package/dist/index.umd.js +12877 -2264
  55. package/dist/index.umd.js.map +1 -1
  56. package/dist/locales/de.d.ts +2 -0
  57. package/dist/locales/en.d.ts +10 -0
  58. package/dist/locales/es.d.ts +10 -0
  59. package/dist/locales/fr.d.ts +2 -0
  60. package/dist/locales/hi.d.ts +2 -0
  61. package/dist/locales/it.d.ts +2 -0
  62. package/dist/locales/pt.d.ts +7 -0
  63. package/dist/types/customization_controller.d.ts +2 -1
  64. package/dist/types/firecms.d.ts +2 -1
  65. package/dist/types/index.d.ts +1 -0
  66. package/dist/types/navigation.d.ts +2 -2
  67. package/dist/types/plugins.d.ts +7 -0
  68. package/dist/types/storage.d.ts +1 -0
  69. package/dist/types/translations.d.ts +646 -0
  70. package/dist/util/useStorageUploadController.d.ts +10 -1
  71. package/package.json +45 -9
  72. package/src/app/Scaffold.tsx +7 -5
  73. package/src/components/AIIcon.tsx +3 -1
  74. package/src/components/ArrayContainer.tsx +6 -4
  75. package/src/components/ClearFilterSortButton.tsx +6 -3
  76. package/src/components/ConfirmationDialog.tsx +4 -2
  77. package/src/components/DeleteEntityDialog.tsx +10 -7
  78. package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
  79. package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
  80. package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
  81. package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
  82. package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
  83. package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +16 -43
  84. package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
  85. package/src/components/EntityCollectionView/EntityCollectionView.tsx +26 -18
  86. package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
  87. package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
  88. package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
  89. package/src/components/EntityCollectionView/ViewModeToggle.tsx +11 -8
  90. package/src/components/EntityView.tsx +3 -2
  91. package/src/components/ErrorBoundary.tsx +27 -15
  92. package/src/components/HomePage/DefaultHomePage.tsx +19 -13
  93. package/src/components/HomePage/HomePageDnD.tsx +3 -1
  94. package/src/components/HomePage/NavigationGroup.tsx +3 -1
  95. package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
  96. package/src/components/LanguageToggle.tsx +66 -0
  97. package/src/components/NotFoundPage.tsx +5 -3
  98. package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
  99. package/src/components/ReferenceWidget.tsx +3 -2
  100. package/src/components/SearchIconsView.tsx +3 -1
  101. package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
  102. package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
  103. package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
  104. package/src/components/UnsavedChangesDialog.tsx +6 -4
  105. package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
  106. package/src/components/VirtualTable/VirtualTableHeader.tsx +12 -10
  107. package/src/components/common/default_entity_actions.tsx +4 -0
  108. package/src/components/common/useDataSourceTableController.tsx +12 -4
  109. package/src/components/index.tsx +1 -0
  110. package/src/core/DefaultAppBar.tsx +14 -10
  111. package/src/core/DefaultDrawer.tsx +8 -2
  112. package/src/core/DrawerNavigationGroup.tsx +5 -3
  113. package/src/core/EntityEditView.tsx +4 -3
  114. package/src/core/EntityEditViewFormActions.tsx +24 -17
  115. package/src/core/EntitySidePanel.tsx +6 -5
  116. package/src/core/FireCMS.tsx +33 -6
  117. package/src/editor/components/SlashCommandMenu.tsx +516 -0
  118. package/src/editor/components/editor-bubble-item.tsx +32 -0
  119. package/src/editor/components/editor-bubble.tsx +118 -0
  120. package/src/editor/components/image-bubble.tsx +156 -0
  121. package/src/editor/components/index.ts +14 -0
  122. package/src/editor/components/table-bubble.tsx +165 -0
  123. package/src/editor/editor.tsx +455 -0
  124. package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
  125. package/src/editor/extensions/Image/index.ts +133 -0
  126. package/src/editor/extensions/Image.ts +159 -0
  127. package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
  128. package/src/editor/extensions/clipboard.ts +72 -0
  129. package/src/editor/extensions/custom-keymap.ts +24 -0
  130. package/src/editor/extensions/drag-and-drop.tsx +480 -0
  131. package/src/editor/hooks/useProseMirror.ts +124 -0
  132. package/src/editor/hooks/useProseMirrorContext.ts +15 -0
  133. package/src/editor/index.ts +2 -0
  134. package/src/editor/markdown.ts +172 -0
  135. package/src/editor/nodeViews/ImageComponent.tsx +20 -0
  136. package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
  137. package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
  138. package/src/editor/nodeViews/index.ts +35 -0
  139. package/src/editor/plugins/index.ts +58 -0
  140. package/src/editor/plugins/inputrules.ts +82 -0
  141. package/src/editor/plugins/placeholderPlugin.ts +55 -0
  142. package/src/editor/plugins/slashCommandPlugin.ts +61 -0
  143. package/src/editor/schema.ts +240 -0
  144. package/src/editor/selectors/ai-selector.tsx +111 -0
  145. package/src/editor/selectors/color-selector.tsx +200 -0
  146. package/src/editor/selectors/link-selector.tsx +118 -0
  147. package/src/editor/selectors/node-selector.tsx +157 -0
  148. package/src/editor/selectors/text-buttons.tsx +86 -0
  149. package/src/editor/types.ts +6 -0
  150. package/src/editor/useProseMirror.ts +126 -0
  151. package/src/editor/utils/prosemirror-utils.ts +108 -0
  152. package/src/editor/utils/remove_classes.ts +17 -0
  153. package/src/editor/utils/useDebouncedCallback.ts +25 -0
  154. package/src/form/EntityForm.tsx +16 -3
  155. package/src/form/EntityFormActions.tsx +19 -12
  156. package/src/form/PropertyFieldBinding.tsx +3 -2
  157. package/src/form/components/LocalChangesMenu.tsx +13 -13
  158. package/src/form/components/StorageItemPreview.tsx +3 -2
  159. package/src/form/components/StorageUploadProgress.tsx +18 -3
  160. package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +4 -4
  161. package/src/form/field_bindings/BlockFieldBinding.tsx +5 -2
  162. package/src/form/field_bindings/KeyValueFieldBinding.tsx +23 -18
  163. package/src/form/field_bindings/MapFieldBinding.tsx +4 -3
  164. package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +33 -19
  165. package/src/form/field_bindings/RepeatFieldBinding.tsx +3 -1
  166. package/src/form/field_bindings/StorageUploadFieldBinding.tsx +4 -3
  167. package/src/hooks/index.tsx +1 -0
  168. package/src/hooks/useBuildNavigationController.tsx +45 -18
  169. package/src/hooks/useCollapsedGroups.ts +7 -6
  170. package/src/hooks/useTranslation.ts +31 -0
  171. package/src/i18n/FireCMSi18nProvider.tsx +160 -0
  172. package/src/index.ts +4 -0
  173. package/src/internal/useBuildSideEntityController.tsx +22 -20
  174. package/src/locales/de.ts +691 -0
  175. package/src/locales/en.ts +703 -0
  176. package/src/locales/es.ts +703 -0
  177. package/src/locales/fr.ts +691 -0
  178. package/src/locales/hi.ts +691 -0
  179. package/src/locales/it.ts +691 -0
  180. package/src/locales/pt.ts +700 -0
  181. package/src/preview/components/UrlComponentPreview.tsx +4 -2
  182. package/src/preview/components/UserPreview.tsx +3 -1
  183. package/src/types/customization_controller.tsx +2 -1
  184. package/src/types/firecms.tsx +2 -1
  185. package/src/types/index.ts +1 -0
  186. package/src/types/navigation.ts +2 -2
  187. package/src/types/plugins.tsx +8 -0
  188. package/src/types/properties.ts +1 -0
  189. package/src/types/storage.ts +2 -1
  190. package/src/types/translations.ts +725 -0
  191. 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";
@@ -212,6 +215,7 @@ export function EntityForm<M extends Record<string, any>>({
212
215
 
213
216
  const sideEntityController = useSideEntityController();
214
217
  const navigationController = useNavigationController();
218
+ const { t } = useTranslation();
215
219
 
216
220
  const navigateBack = useCallback(() => {
217
221
  if (openEntityMode === "side_panel") {
@@ -366,6 +370,15 @@ export function EntityForm<M extends Record<string, any>>({
366
370
  useEffect(() => {
367
371
 
368
372
  const handleKeyDown = (e: KeyboardEvent) => {
373
+ if (e.defaultPrevented) return;
374
+ const activeElement = document.activeElement as HTMLElement;
375
+ const isInput = activeElement && (
376
+ activeElement.tagName === "INPUT" ||
377
+ activeElement.tagName === "TEXTAREA" ||
378
+ activeElement.isContentEditable
379
+ );
380
+ if (isInput) return;
381
+
369
382
  const isUndo = (e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z";
370
383
  const isRedo =
371
384
  ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "z") ||
@@ -767,7 +780,7 @@ export function EntityForm<M extends Record<string, any>>({
767
780
 
768
781
  {!entity?.values && initialStatus === "existing" &&
769
782
  <Alert color={"warning"} size={"small"} outerClassName={"w-full mb-4 text-xs"}>
770
- This entity does not exist in the database
783
+ {t("this_entity_not_exist")}
771
784
  </Alert>}
772
785
 
773
786
  {showEntityPath && <Alert color={"base"} outerClassName={"w-full"} size={"small"}>
@@ -781,7 +794,7 @@ export function EntityForm<M extends Record<string, any>>({
781
794
  {children}
782
795
 
783
796
  {initialEntityId && !entity && initialStatus !== "new" && <Alert color={"info"} size={"small"}>
784
- This entity does not exist in the database
797
+ {t("this_entity_not_exist")}
785
798
  </Alert>}
786
799
 
787
800
  {!Builder && !collection.hideIdFromForm &&
@@ -19,6 +19,7 @@ import {
19
19
  } from "@firecms/ui";
20
20
  import { FormexController } from "@firecms/formex";
21
21
  import { useFireCMSContext, useSideEntityController } from "../hooks";
22
+ import { useTranslation } from "../hooks/useTranslation";
22
23
 
23
24
  export interface EntityFormActionsProps {
24
25
  fullPath: string;
@@ -56,6 +57,7 @@ export function EntityFormActions({
56
57
 
57
58
  const context = useFireCMSContext();
58
59
  const sideEntityController = useSideEntityController();
60
+ const { t } = useTranslation();
59
61
 
60
62
  return layout === "bottom"
61
63
  ? buildBottomActions({
@@ -72,7 +74,8 @@ export function EntityFormActions({
72
74
  openEntityMode,
73
75
  navigateBack,
74
76
  formContext,
75
- formex
77
+ formex,
78
+ t
76
79
  })
77
80
  : buildSideActions({
78
81
  fullPath,
@@ -88,7 +91,8 @@ export function EntityFormActions({
88
91
  openEntityMode,
89
92
  navigateBack,
90
93
  formContext,
91
- formex
94
+ formex,
95
+ t
92
96
  });
93
97
  }
94
98
 
@@ -108,6 +112,7 @@ type ActionsViewProps<M extends object> = {
108
112
  navigateBack: () => void;
109
113
  formContext: FormContext,
110
114
  formex: FormexController<any>;
115
+ t: (key: any, vars?: Record<string, string>) => string;
111
116
  };
112
117
 
113
118
  function buildBottomActions<M extends object>({
@@ -125,7 +130,8 @@ function buildBottomActions<M extends object>({
125
130
  openEntityMode,
126
131
  navigateBack,
127
132
  formContext,
128
- formex
133
+ formex,
134
+ t
129
135
  }: ActionsViewProps<M>) {
130
136
 
131
137
  const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
@@ -165,16 +171,16 @@ function buildBottomActions<M extends object>({
165
171
  <Button variant="text" disabled={disabled || formex.isSubmitting}
166
172
  color={"primary"}
167
173
  type="reset">
168
- {status === "existing" ? "Discard" : "Clear"}
174
+ {status === "existing" ? t("discard") : t("clear")}
169
175
  </Button>
170
176
  <Button variant={"filled"}
171
177
  color="primary"
172
178
  type="submit"
173
179
  disabled={disabled || formex.isSubmitting}
174
180
  startIcon={hasErrors ? <ErrorIcon/> : undefined}>
175
- {status === "existing" && "Save"}
176
- {status === "copy" && "Create copy"}
177
- {status === "new" && "Create"}
181
+ {status === "existing" && t("save")}
182
+ {status === "copy" && t("create_copy")}
183
+ {status === "new" && t("create")}
178
184
  </Button>
179
185
 
180
186
  </DialogActions>;
@@ -193,7 +199,8 @@ function buildSideActions<M extends object>({
193
199
  disabled,
194
200
  status,
195
201
  pluginActions,
196
- formex
202
+ formex,
203
+ t
197
204
  }: ActionsViewProps<M>) {
198
205
 
199
206
  const hasErrors = Object.keys(formex.errors).length > 0 && formex.submitCount > 0;
@@ -207,12 +214,12 @@ function buildSideActions<M extends object>({
207
214
  size={"large"}
208
215
  startIcon={hasErrors ? <ErrorIcon/> : undefined}
209
216
  disabled={disabled || formex.isSubmitting}>
210
- {status === "existing" && "Save"}
211
- {status === "copy" && "Create copy"}
212
- {status === "new" && "Create"}
217
+ {status === "existing" && t("save")}
218
+ {status === "copy" && t("create_copy")}
219
+ {status === "new" && t("create")}
213
220
  </LoadingButton>
214
221
  <Button fullWidth={true} variant="text" disabled={disabled || formex.isSubmitting} type="reset">
215
- {status === "existing" ? "Discard" : "Clear"}
222
+ {status === "existing" ? t("discard") : t("clear")}
216
223
  </Button>
217
224
 
218
225
  {pluginActions}