@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.
- package/dist/components/EntityCollectionView/CollectionDataErrorBanner.d.ts +4 -0
- package/dist/components/ErrorBoundary.d.ts +3 -1
- package/dist/components/HomePage/DefaultHomePage.d.ts +0 -1
- package/dist/components/LanguageToggle.d.ts +1 -0
- package/dist/components/UnsavedChangesDialog.d.ts +1 -0
- package/dist/components/VirtualTable/VirtualTableHeader.d.ts +1 -0
- package/dist/components/VirtualTable/VirtualTableHeaderRow.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTableProps.d.ts +6 -1
- package/dist/components/VirtualTable/types.d.ts +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/core/DrawerNavigationGroup.d.ts +2 -2
- package/dist/editor/components/SlashCommandMenu.d.ts +6 -0
- package/dist/editor/components/editor-bubble-item.d.ts +8 -0
- package/dist/editor/components/editor-bubble.d.ts +8 -0
- package/dist/editor/components/image-bubble.d.ts +5 -0
- package/dist/editor/components/index.d.ts +16 -0
- package/dist/editor/components/table-bubble.d.ts +5 -0
- package/dist/editor/editor.d.ts +30 -0
- package/dist/editor/extensions/HighlightDecorationExtension.d.ts +24 -0
- package/dist/editor/extensions/Image/index.d.ts +6 -0
- package/dist/editor/extensions/Image.d.ts +6 -0
- package/dist/editor/extensions/TextLoadingDecorationExtension.d.ts +16 -0
- package/dist/editor/extensions/clipboard.d.ts +7 -0
- package/dist/editor/extensions/custom-keymap.d.ts +1 -0
- package/dist/editor/extensions/drag-and-drop.d.ts +9 -0
- package/dist/editor/hooks/useProseMirror.d.ts +13 -0
- package/dist/editor/hooks/useProseMirrorContext.d.ts +9 -0
- package/dist/editor/index.d.ts +2 -0
- package/dist/editor/markdown.d.ts +5 -0
- package/dist/editor/nodeViews/ImageComponent.d.ts +3 -0
- package/dist/editor/nodeViews/ReactNodeView.d.ts +29 -0
- package/dist/editor/nodeViews/TaskItemComponent.d.ts +3 -0
- package/dist/editor/nodeViews/index.d.ts +6 -0
- package/dist/editor/plugins/index.d.ts +2 -0
- package/dist/editor/plugins/inputrules.d.ts +6 -0
- package/dist/editor/plugins/placeholderPlugin.d.ts +3 -0
- package/dist/editor/plugins/slashCommandPlugin.d.ts +12 -0
- package/dist/editor/schema.d.ts +2 -0
- package/dist/editor/selectors/ai-selector.d.ts +0 -0
- package/dist/editor/selectors/color-selector.d.ts +10 -0
- package/dist/editor/selectors/link-selector.d.ts +8 -0
- package/dist/editor/selectors/node-selector.d.ts +15 -0
- package/dist/editor/selectors/text-buttons.d.ts +1 -0
- package/dist/editor/types.d.ts +5 -0
- package/dist/editor/useProseMirror.d.ts +16 -0
- package/dist/editor/utils/prosemirror-utils.d.ts +6 -0
- package/dist/editor/utils/remove_classes.d.ts +1 -0
- package/dist/editor/utils/useDebouncedCallback.d.ts +1 -0
- package/dist/form/field_bindings/MapFieldBinding.d.ts +1 -1
- package/dist/form/field_bindings/MarkdownEditorFieldBinding.d.ts +1 -1
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useBuildNavigationController.d.ts +0 -1
- package/dist/hooks/useCollapsedGroups.d.ts +3 -3
- package/dist/hooks/useTranslation.d.ts +17 -0
- package/dist/i18n/FireCMSi18nProvider.d.ts +33 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.es.js +29889 -18645
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +29883 -18659
- package/dist/index.umd.js.map +1 -1
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/en.d.ts +10 -0
- package/dist/locales/es.d.ts +10 -0
- package/dist/locales/fr.d.ts +2 -0
- package/dist/locales/hi.d.ts +2 -0
- package/dist/locales/it.d.ts +2 -0
- package/dist/locales/pt.d.ts +7 -0
- package/dist/types/collections.d.ts +38 -0
- package/dist/types/customization_controller.d.ts +2 -1
- package/dist/types/firecms.d.ts +2 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/navigation.d.ts +2 -2
- package/dist/types/plugins.d.ts +7 -0
- package/dist/types/properties.d.ts +9 -8
- package/dist/types/storage.d.ts +1 -0
- package/dist/types/translations.d.ts +669 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/lazy_eager.d.ts +7 -0
- package/dist/util/objects.d.ts +1 -0
- package/dist/util/useStorageUploadController.d.ts +10 -1
- package/package.json +45 -9
- package/src/app/Scaffold.tsx +7 -5
- package/src/components/AIIcon.tsx +3 -1
- package/src/components/ArrayContainer.tsx +6 -4
- package/src/components/ClearFilterSortButton.tsx +6 -3
- package/src/components/ConfirmationDialog.tsx +4 -2
- package/src/components/DeleteEntityDialog.tsx +10 -7
- package/src/components/EntityCollectionTable/EntityCollectionRowActions.tsx +9 -3
- package/src/components/EntityCollectionTable/fields/TableReferenceField.tsx +6 -3
- package/src/components/EntityCollectionTable/internal/CollectionTableToolbar.tsx +3 -1
- package/src/components/EntityCollectionTable/internal/popup_field/PopupFormField.tsx +3 -2
- package/src/components/EntityCollectionView/BoardSortableList.tsx +3 -1
- package/src/components/EntityCollectionView/CollectionDataErrorBanner.tsx +43 -0
- package/src/components/EntityCollectionView/EntityCollectionBoardView.tsx +16 -43
- package/src/components/EntityCollectionView/EntityCollectionCardView.tsx +17 -25
- package/src/components/EntityCollectionView/EntityCollectionView.tsx +24 -18
- package/src/components/EntityCollectionView/EntityCollectionViewActions.tsx +4 -3
- package/src/components/EntityCollectionView/EntityCollectionViewStartActions.tsx +4 -2
- package/src/components/EntityCollectionView/FiltersDialog.tsx +8 -5
- package/src/components/EntityCollectionView/ViewModeToggle.tsx +11 -8
- package/src/components/EntityJsonPreview.tsx +2 -1
- package/src/components/EntityView.tsx +3 -2
- package/src/components/ErrorBoundary.tsx +27 -15
- package/src/components/HomePage/DefaultHomePage.tsx +19 -13
- package/src/components/HomePage/HomePageDnD.tsx +3 -1
- package/src/components/HomePage/NavigationGroup.tsx +3 -1
- package/src/components/HomePage/RenameGroupDialog.tsx +15 -13
- package/src/components/LanguageToggle.tsx +66 -0
- package/src/components/NotFoundPage.tsx +5 -3
- package/src/components/ReferenceTable/ReferenceSelectionTable.tsx +9 -7
- package/src/components/ReferenceWidget.tsx +3 -2
- package/src/components/SearchIconsView.tsx +3 -1
- package/src/components/SelectableTable/filters/DateTimeFilterField.tsx +11 -0
- package/src/components/SelectableTable/filters/ReferenceFilterField.tsx +15 -2
- package/src/components/SelectableTable/filters/StringNumberFilterField.tsx +11 -0
- package/src/components/UnsavedChangesDialog.tsx +6 -4
- package/src/components/VirtualTable/VirtualTable.performance.test.tsx +1 -0
- package/src/components/VirtualTable/VirtualTable.tsx +5 -3
- package/src/components/VirtualTable/VirtualTableHeader.tsx +21 -18
- package/src/components/VirtualTable/VirtualTableHeaderRow.tsx +8 -3
- package/src/components/VirtualTable/VirtualTableProps.tsx +7 -1
- package/src/components/VirtualTable/types.tsx +1 -0
- package/src/components/common/default_entity_actions.tsx +4 -0
- package/src/components/common/useDataSourceTableController.tsx +5 -14
- package/src/components/index.tsx +1 -0
- package/src/core/DefaultAppBar.tsx +14 -10
- package/src/core/DefaultDrawer.tsx +8 -2
- package/src/core/DrawerNavigationGroup.tsx +5 -3
- package/src/core/EntityEditView.tsx +53 -7
- package/src/core/EntityEditViewFormActions.tsx +24 -17
- package/src/core/EntitySidePanel.tsx +6 -4
- package/src/core/FireCMS.tsx +33 -6
- package/src/core/field_configs.tsx +4 -2
- package/src/editor/components/SlashCommandMenu.tsx +516 -0
- package/src/editor/components/editor-bubble-item.tsx +32 -0
- package/src/editor/components/editor-bubble.tsx +118 -0
- package/src/editor/components/image-bubble.tsx +156 -0
- package/src/editor/components/index.ts +14 -0
- package/src/editor/components/table-bubble.tsx +165 -0
- package/src/editor/editor.tsx +455 -0
- package/src/editor/extensions/HighlightDecorationExtension.ts +114 -0
- package/src/editor/extensions/Image/index.ts +133 -0
- package/src/editor/extensions/Image.ts +159 -0
- package/src/editor/extensions/TextLoadingDecorationExtension.tsx +107 -0
- package/src/editor/extensions/clipboard.ts +72 -0
- package/src/editor/extensions/custom-keymap.ts +24 -0
- package/src/editor/extensions/drag-and-drop.tsx +480 -0
- package/src/editor/hooks/useProseMirror.ts +124 -0
- package/src/editor/hooks/useProseMirrorContext.ts +15 -0
- package/src/editor/index.ts +2 -0
- package/src/editor/markdown.ts +172 -0
- package/src/editor/nodeViews/ImageComponent.tsx +20 -0
- package/src/editor/nodeViews/ReactNodeView.tsx +89 -0
- package/src/editor/nodeViews/TaskItemComponent.tsx +29 -0
- package/src/editor/nodeViews/index.ts +35 -0
- package/src/editor/plugins/index.ts +58 -0
- package/src/editor/plugins/inputrules.ts +82 -0
- package/src/editor/plugins/placeholderPlugin.ts +55 -0
- package/src/editor/plugins/slashCommandPlugin.ts +61 -0
- package/src/editor/schema.ts +240 -0
- package/src/editor/selectors/ai-selector.tsx +111 -0
- package/src/editor/selectors/color-selector.tsx +200 -0
- package/src/editor/selectors/link-selector.tsx +118 -0
- package/src/editor/selectors/node-selector.tsx +157 -0
- package/src/editor/selectors/text-buttons.tsx +86 -0
- package/src/editor/types.ts +6 -0
- package/src/editor/useProseMirror.ts +126 -0
- package/src/editor/utils/prosemirror-utils.ts +108 -0
- package/src/editor/utils/remove_classes.ts +17 -0
- package/src/editor/utils/useDebouncedCallback.ts +25 -0
- package/src/form/EntityForm.tsx +80 -7
- package/src/form/EntityFormActions.tsx +19 -12
- package/src/form/PropertyFieldBinding.tsx +7 -5
- package/src/form/components/LocalChangesMenu.tsx +13 -13
- package/src/form/components/StorageItemPreview.tsx +3 -2
- package/src/form/components/StorageUploadProgress.tsx +18 -3
- package/src/form/field_bindings/ArrayCustomShapedFieldBinding.tsx +18 -5
- package/src/form/field_bindings/ArrayOfReferencesFieldBinding.tsx +22 -9
- package/src/form/field_bindings/BlockFieldBinding.tsx +26 -9
- package/src/form/field_bindings/DateTimeFieldBinding.tsx +1 -1
- package/src/form/field_bindings/KeyValueFieldBinding.tsx +46 -24
- package/src/form/field_bindings/MapFieldBinding.tsx +27 -11
- package/src/form/field_bindings/MarkdownEditorFieldBinding.tsx +73 -36
- package/src/form/field_bindings/MultiSelectFieldBinding.tsx +15 -1
- package/src/form/field_bindings/ReferenceAsStringFieldBinding.tsx +25 -11
- package/src/form/field_bindings/ReferenceFieldBinding.tsx +25 -11
- package/src/form/field_bindings/RepeatFieldBinding.tsx +21 -6
- package/src/form/field_bindings/SelectFieldBinding.tsx +7 -5
- package/src/form/field_bindings/StorageUploadFieldBinding.tsx +28 -10
- package/src/form/field_bindings/SwitchFieldBinding.tsx +31 -14
- package/src/form/field_bindings/TextFieldBinding.tsx +10 -7
- package/src/form/field_bindings/UserSelectFieldBinding.tsx +7 -5
- package/src/hooks/index.tsx +1 -0
- package/src/hooks/useBuildNavigationController.tsx +20 -13
- package/src/hooks/useCollapsedGroups.ts +7 -6
- package/src/hooks/useTranslation.ts +31 -0
- package/src/i18n/FireCMSi18nProvider.tsx +160 -0
- package/src/index.ts +5 -0
- package/src/locales/de.ts +718 -0
- package/src/locales/en.ts +730 -0
- package/src/locales/es.ts +730 -0
- package/src/locales/fr.ts +718 -0
- package/src/locales/hi.ts +718 -0
- package/src/locales/it.ts +718 -0
- package/src/locales/pt.ts +727 -0
- package/src/preview/PropertyPreview.tsx +3 -2
- package/src/preview/components/ReferencePreview.tsx +2 -1
- package/src/preview/components/UrlComponentPreview.tsx +4 -2
- package/src/preview/components/UserPreview.tsx +3 -1
- package/src/preview/property_previews/MapPropertyPreview.tsx +49 -27
- package/src/routes/FireCMSRoute.tsx +63 -54
- package/src/types/collections.ts +40 -0
- package/src/types/customization_controller.tsx +2 -1
- package/src/types/firecms.tsx +2 -1
- package/src/types/index.ts +1 -0
- package/src/types/navigation.ts +2 -2
- package/src/types/plugins.tsx +8 -0
- package/src/types/properties.ts +12 -10
- package/src/types/storage.ts +2 -1
- package/src/types/translations.ts +752 -0
- package/src/util/index.ts +1 -0
- package/src/util/lazy_eager.tsx +33 -0
- package/src/util/objects.ts +15 -0
- 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,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
|
+
}
|
package/src/form/EntityForm.tsx
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
857
|
+
{t("this_entity_not_exist")}
|
|
785
858
|
</Alert>}
|
|
786
859
|
|
|
787
860
|
{!Builder && !collection.hideIdFromForm &&
|