@alpaca-editor/core 1.0.3815 → 1.0.3818
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/config/config.js +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/editor/EditorWarnings.js +1 -1
- package/dist/editor/EditorWarnings.js.map +1 -1
- package/dist/editor/FieldList.js +1 -1
- package/dist/editor/FieldList.js.map +1 -1
- package/dist/editor/FieldListFieldWithFallbacks.js +3 -3
- package/dist/editor/FieldListFieldWithFallbacks.js.map +1 -1
- package/dist/editor/Titlebar.js +1 -1
- package/dist/editor/Titlebar.js.map +1 -1
- package/dist/editor/client/EditorClient.js +71 -31
- package/dist/editor/client/EditorClient.js.map +1 -1
- package/dist/editor/client/editContext.d.ts +9 -3
- package/dist/editor/client/editContext.js.map +1 -1
- package/dist/editor/client/operations.d.ts +5 -1
- package/dist/editor/client/operations.js +97 -3
- package/dist/editor/client/operations.js.map +1 -1
- package/dist/editor/component-designer/ComponentDesigner.js +1 -1
- package/dist/editor/component-designer/ComponentDesigner.js.map +1 -1
- package/dist/editor/menubar/LanguageSelector.js +3 -3
- package/dist/editor/menubar/LanguageSelector.js.map +1 -1
- package/dist/editor/menubar/PageSelector.js +1 -1
- package/dist/editor/menubar/PageSelector.js.map +1 -1
- package/dist/editor/menubar/PageViewerControls.js +12 -7
- package/dist/editor/menubar/PageViewerControls.js.map +1 -1
- package/dist/editor/menubar/Separator.js +1 -1
- package/dist/editor/menubar/VersionSelector.js +1 -1
- package/dist/editor/menubar/VersionSelector.js.map +1 -1
- package/dist/editor/page-editor-chrome/FrameMenu.d.ts +2 -2
- package/dist/editor/page-editor-chrome/FrameMenu.js +21 -15
- package/dist/editor/page-editor-chrome/FrameMenu.js.map +1 -1
- package/dist/editor/page-editor-chrome/FrameMenus.d.ts +2 -2
- package/dist/editor/page-editor-chrome/FrameMenus.js +2 -2
- package/dist/editor/page-editor-chrome/FrameMenus.js.map +1 -1
- package/dist/editor/page-editor-chrome/InlineEditor.d.ts +2 -2
- package/dist/editor/page-editor-chrome/InlineEditor.js +175 -17
- package/dist/editor/page-editor-chrome/InlineEditor.js.map +1 -1
- package/dist/editor/page-editor-chrome/PageEditorChrome.d.ts +2 -2
- package/dist/editor/page-editor-chrome/PageEditorChrome.js +2 -2
- package/dist/editor/page-editor-chrome/PageEditorChrome.js.map +1 -1
- package/dist/editor/page-viewer/EditorForm.d.ts +2 -1
- package/dist/editor/page-viewer/EditorForm.js +9 -8
- package/dist/editor/page-viewer/EditorForm.js.map +1 -1
- package/dist/editor/page-viewer/MiniMap.d.ts +2 -2
- package/dist/editor/page-viewer/MiniMap.js +2 -2
- package/dist/editor/page-viewer/MiniMap.js.map +1 -1
- package/dist/editor/page-viewer/PageViewer.d.ts +2 -2
- package/dist/editor/page-viewer/PageViewer.js +3 -3
- package/dist/editor/page-viewer/PageViewer.js.map +1 -1
- package/dist/editor/page-viewer/PageViewerFrame.d.ts +2 -2
- package/dist/editor/page-viewer/PageViewerFrame.js +12 -12
- package/dist/editor/page-viewer/PageViewerFrame.js.map +1 -1
- package/dist/editor/reviews/Comments.d.ts +2 -0
- package/dist/editor/reviews/Comments.js +26 -9
- package/dist/editor/reviews/Comments.js.map +1 -1
- package/dist/editor/reviews/DiffView.d.ts +17 -0
- package/dist/editor/reviews/DiffView.js +57 -0
- package/dist/editor/reviews/DiffView.js.map +1 -0
- package/dist/editor/reviews/SuggestedEdit.d.ts +4 -0
- package/dist/editor/reviews/SuggestedEdit.js +180 -0
- package/dist/editor/reviews/SuggestedEdit.js.map +1 -0
- package/dist/editor/services/suggestedEditsService.d.ts +17 -0
- package/dist/editor/services/suggestedEditsService.js +26 -0
- package/dist/editor/services/suggestedEditsService.js.map +1 -0
- package/dist/editor/ui/PerfectTree.js +3 -3
- package/dist/editor/ui/PerfectTree.js.map +1 -1
- package/dist/editor/ui/SimpleIconButton.js +3 -1
- package/dist/editor/ui/SimpleIconButton.js.map +1 -1
- package/dist/editor/views/CompareView.js +4 -13
- package/dist/editor/views/CompareView.js.map +1 -1
- package/dist/editor/views/EditView.js +2 -2
- package/dist/editor/views/EditView.js.map +1 -1
- package/dist/editor/views/SingleEditView.d.ts +2 -2
- package/dist/editor/views/SingleEditView.js +2 -2
- package/dist/editor/views/SingleEditView.js.map +1 -1
- package/dist/lib/safelist.js +1 -1
- package/dist/lib/safelist.js.map +1 -1
- package/dist/page-wizard/steps/BuildPageStep.js +2 -2
- package/dist/page-wizard/steps/BuildPageStep.js.map +1 -1
- package/dist/page-wizard/steps/CreatePageAndLayoutStep.js +2 -2
- package/dist/page-wizard/steps/CreatePageAndLayoutStep.js.map +1 -1
- package/dist/styles.css +36 -2
- package/dist/types.d.ts +18 -0
- package/package.json +4 -1
- package/src/config/config.tsx +2 -2
- package/src/editor/EditorWarnings.tsx +2 -2
- package/src/editor/FieldList.tsx +6 -6
- package/src/editor/FieldListFieldWithFallbacks.tsx +9 -9
- package/src/editor/Titlebar.tsx +4 -4
- package/src/editor/client/EditorClient.tsx +83 -51
- package/src/editor/client/editContext.ts +12 -3
- package/src/editor/client/operations.ts +146 -9
- package/src/editor/component-designer/ComponentDesigner.tsx +1 -1
- package/src/editor/menubar/LanguageSelector.tsx +6 -6
- package/src/editor/menubar/PageSelector.tsx +11 -11
- package/src/editor/menubar/PageViewerControls.tsx +49 -23
- package/src/editor/menubar/Separator.tsx +2 -2
- package/src/editor/menubar/VersionSelector.tsx +1 -1
- package/src/editor/page-editor-chrome/FrameMenu.tsx +18 -17
- package/src/editor/page-editor-chrome/FrameMenus.tsx +6 -6
- package/src/editor/page-editor-chrome/InlineEditor.tsx +233 -22
- package/src/editor/page-editor-chrome/PageEditorChrome.tsx +11 -14
- package/src/editor/page-viewer/EditorForm.tsx +15 -9
- package/src/editor/page-viewer/MiniMap.tsx +4 -4
- package/src/editor/page-viewer/PageViewer.tsx +6 -6
- package/src/editor/page-viewer/PageViewerFrame.tsx +19 -13
- package/src/editor/reviews/Comments.tsx +56 -15
- package/src/editor/reviews/DiffView.tsx +109 -0
- package/src/editor/reviews/SuggestedEdit.tsx +316 -0
- package/src/editor/services/suggestedEditsService.ts +39 -0
- package/src/editor/ui/PerfectTree.tsx +5 -5
- package/src/editor/ui/SimpleIconButton.tsx +5 -3
- package/src/editor/views/CompareView.tsx +13 -24
- package/src/editor/views/EditView.tsx +2 -2
- package/src/editor/views/SingleEditView.tsx +3 -3
- package/src/lib/safelist.tsx +2 -0
- package/src/page-wizard/steps/BuildPageStep.tsx +18 -25
- package/src/page-wizard/steps/CreatePageAndLayoutStep.tsx +16 -18
- package/src/types.ts +19 -0
|
@@ -49,7 +49,7 @@ export function PageSelector({
|
|
|
49
49
|
<>
|
|
50
50
|
<div
|
|
51
51
|
id="page-selector-button"
|
|
52
|
-
className="
|
|
52
|
+
className="flex cursor-pointer items-center gap-3 rounded-md p-[7px] py-[5px] text-sm text-gray-200 hover:bg-gray-500"
|
|
53
53
|
onClick={(ev) => overlaypanel.current?.toggle(ev, ev.currentTarget)}
|
|
54
54
|
data-testid="page-selector-button"
|
|
55
55
|
>
|
|
@@ -66,10 +66,10 @@ export function PageSelector({
|
|
|
66
66
|
</div>
|
|
67
67
|
</div>
|
|
68
68
|
<OverlayPanel dismissable={true} ref={overlaypanel} closeOnEscape>
|
|
69
|
-
<div className="h-[75vh] min-w-48 flex
|
|
70
|
-
<div className="flex-1 flex
|
|
71
|
-
<div className="
|
|
72
|
-
<div className="
|
|
69
|
+
<div className="flex h-[75vh] min-w-48 flex-col overflow-hidden">
|
|
70
|
+
<div className="flex flex-1 flex-col gap-1">
|
|
71
|
+
<div className="flex flex-col p-2">
|
|
72
|
+
<div className="mb-2 flex items-center gap-1 text-xs text-gray-500">
|
|
73
73
|
<ArrowDownIcon /> Search
|
|
74
74
|
</div>
|
|
75
75
|
<ItemSearch
|
|
@@ -78,8 +78,8 @@ export function PageSelector({
|
|
|
78
78
|
itemSelected={(item) => loadItem(item)}
|
|
79
79
|
/>
|
|
80
80
|
</div>
|
|
81
|
-
<div className="flex-1 flex
|
|
82
|
-
<div className="border-t p-2 pb-1 text-xs text-gray-500
|
|
81
|
+
<div className="flex flex-1 flex-col">
|
|
82
|
+
<div className="flex items-center gap-1 border-t p-2 pb-1 text-xs text-gray-500">
|
|
83
83
|
<ArrowDownIcon /> Select
|
|
84
84
|
</div>
|
|
85
85
|
<div className="relative flex-1">
|
|
@@ -91,8 +91,8 @@ export function PageSelector({
|
|
|
91
91
|
</div>
|
|
92
92
|
</div>
|
|
93
93
|
</div>
|
|
94
|
-
<div className="border-t p-2
|
|
95
|
-
<div className="
|
|
94
|
+
<div className="flex flex-col border-t p-2">
|
|
95
|
+
<div className="mb-2 flex items-center gap-1 text-xs text-gray-500">
|
|
96
96
|
<ArrowDownIcon /> Last visited
|
|
97
97
|
</div>
|
|
98
98
|
<BrowseHistory
|
|
@@ -101,8 +101,8 @@ export function PageSelector({
|
|
|
101
101
|
/>
|
|
102
102
|
</div>
|
|
103
103
|
</div>
|
|
104
|
-
<div className="border-t p-2
|
|
105
|
-
<div className="
|
|
104
|
+
<div className="flex flex-col border-t p-2">
|
|
105
|
+
<div className="mb-2 flex items-center gap-1 text-xs text-gray-500">
|
|
106
106
|
<ArrowDownIcon /> Actions
|
|
107
107
|
</div>
|
|
108
108
|
<div className="flex gap-2">
|
|
@@ -3,6 +3,7 @@ import { useEditContext } from "../client/editContext";
|
|
|
3
3
|
import { Separator } from "./Separator";
|
|
4
4
|
import { CompareIcon, FormEditIcon } from "../ui/Icons";
|
|
5
5
|
import { SimpleIconButton } from "../ui/SimpleIconButton";
|
|
6
|
+
import { Route, SquarePen, UserRoundPen, EyeIcon, Pencil } from "lucide-react";
|
|
6
7
|
|
|
7
8
|
export function PageViewerControls() {
|
|
8
9
|
const editContext = useEditContext();
|
|
@@ -17,16 +18,54 @@ export function PageViewerControls() {
|
|
|
17
18
|
const setDevice = pageViewContext.setDevice;
|
|
18
19
|
|
|
19
20
|
return (
|
|
20
|
-
<div className="flex gap-2
|
|
21
|
+
<div className="flex items-center gap-2">
|
|
21
22
|
{hasLayout && (
|
|
22
23
|
<>
|
|
23
|
-
{
|
|
24
|
+
{!editContext.user?.isLimitedPreviewUser && (
|
|
25
|
+
<SimpleIconButton
|
|
26
|
+
icon={<Pencil className="h-6 w-6 p-1" />}
|
|
27
|
+
label="Edit"
|
|
28
|
+
size="large"
|
|
29
|
+
className={classNames(
|
|
30
|
+
editContext.mode === "edit"
|
|
31
|
+
? "bg-gray-200"
|
|
32
|
+
: "hover:bg-gray-200 hover:text-gray-800",
|
|
33
|
+
)}
|
|
34
|
+
onClick={() => editContext.setMode("edit")}
|
|
35
|
+
/>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
<SimpleIconButton
|
|
39
|
+
icon={<EyeIcon className="h-6 w-6 p-1" />}
|
|
40
|
+
label="Preview"
|
|
41
|
+
size="large"
|
|
42
|
+
className={classNames(
|
|
43
|
+
editContext.mode === "preview"
|
|
44
|
+
? "bg-gray-200"
|
|
45
|
+
: "hover:bg-gray-200 hover:text-gray-800",
|
|
46
|
+
)}
|
|
47
|
+
onClick={() => editContext.setMode("preview")}
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
<SimpleIconButton
|
|
51
|
+
selected={editContext?.mode === "suggestions"}
|
|
52
|
+
icon={<UserRoundPen size={24} className="p-0.5" />}
|
|
53
|
+
label="Suggestions"
|
|
54
|
+
size="large"
|
|
55
|
+
className={classNames(
|
|
56
|
+
editContext?.mode === "suggestions"
|
|
57
|
+
? "text-gray-600"
|
|
58
|
+
: "hover:text-gray-600",
|
|
59
|
+
)}
|
|
60
|
+
onClick={() => editContext?.setMode("suggestions")}
|
|
61
|
+
/>
|
|
62
|
+
<Separator size="large" />
|
|
24
63
|
<i
|
|
25
64
|
className={classNames(
|
|
26
65
|
device === "desktop"
|
|
27
66
|
? "bg-gray-200"
|
|
28
|
-
: " hover:bg-gray-200
|
|
29
|
-
"pi pi-desktop cursor-pointer p-2
|
|
67
|
+
: "text-gray-400 hover:bg-gray-200 hover:text-gray-800",
|
|
68
|
+
"pi pi-desktop cursor-pointer rounded-full p-2",
|
|
30
69
|
)}
|
|
31
70
|
title="Desktop"
|
|
32
71
|
onClick={() => {
|
|
@@ -37,8 +76,8 @@ export function PageViewerControls() {
|
|
|
37
76
|
className={classNames(
|
|
38
77
|
device && device !== "desktop"
|
|
39
78
|
? "bg-gray-200"
|
|
40
|
-
: " hover:bg-gray-200
|
|
41
|
-
"pi pi-mobile cursor-pointer p-2
|
|
79
|
+
: "text-gray-400 hover:bg-gray-200 hover:text-gray-800",
|
|
80
|
+
"pi pi-mobile cursor-pointer rounded-full p-2",
|
|
42
81
|
)}
|
|
43
82
|
title="Mobile"
|
|
44
83
|
onClick={() => {
|
|
@@ -51,8 +90,8 @@ export function PageViewerControls() {
|
|
|
51
90
|
className={classNames(
|
|
52
91
|
!device
|
|
53
92
|
? "bg-gray-200"
|
|
54
|
-
: " hover:bg-gray-200
|
|
55
|
-
"
|
|
93
|
+
: "text-gray-400 hover:bg-gray-200 hover:text-gray-800",
|
|
94
|
+
"h-8 w-8 cursor-pointer rounded-full p-1",
|
|
56
95
|
)}
|
|
57
96
|
title="Form"
|
|
58
97
|
onClick={() => {
|
|
@@ -63,27 +102,14 @@ export function PageViewerControls() {
|
|
|
63
102
|
</i>
|
|
64
103
|
<Separator size="large" />
|
|
65
104
|
<i
|
|
66
|
-
className="pi pi-external-link cursor-pointer
|
|
105
|
+
className="pi pi-external-link cursor-pointer rounded-full p-2 text-gray-400 hover:bg-gray-200 hover:text-gray-800"
|
|
67
106
|
title="Fullscreen"
|
|
68
107
|
onClick={() => pageViewContext.setFullscreen(true)}
|
|
69
108
|
/>
|
|
70
|
-
{!editContext.user?.isLimitedPreviewUser && (
|
|
71
|
-
<i
|
|
72
|
-
className={classNames(
|
|
73
|
-
editContext.previewMode
|
|
74
|
-
? "bg-gray-200"
|
|
75
|
-
: " hover:bg-gray-200 text-gray-100 hover:text-gray-800",
|
|
76
|
-
"pi pi-eye cursor-pointer p-2 rounded-full"
|
|
77
|
-
)}
|
|
78
|
-
title="Preview"
|
|
79
|
-
onClick={() => editContext.setPreviewMode((x) => !x)}
|
|
80
|
-
/>
|
|
81
|
-
)}
|
|
82
|
-
<Separator size="large" />
|
|
83
109
|
</>
|
|
84
110
|
)}
|
|
85
111
|
<SimpleIconButton
|
|
86
|
-
icon={<CompareIcon className="
|
|
112
|
+
icon={<CompareIcon className="h-6 w-6 p-1" />}
|
|
87
113
|
label="Compare"
|
|
88
114
|
size="large"
|
|
89
115
|
className={
|
|
@@ -4,8 +4,8 @@ export function Separator({ size }: { size?: "large" | "small" }) {
|
|
|
4
4
|
return (
|
|
5
5
|
<div
|
|
6
6
|
className={classNames(
|
|
7
|
-
"border-r border-gray-
|
|
8
|
-
size === "large" ? "h-7
|
|
7
|
+
"border-r border-gray-400",
|
|
8
|
+
size === "large" ? "mx-3 h-7" : "mx-1 h-4",
|
|
9
9
|
)}
|
|
10
10
|
></div>
|
|
11
11
|
);
|
|
@@ -40,7 +40,7 @@ export function VersionSelector({
|
|
|
40
40
|
<>
|
|
41
41
|
<div
|
|
42
42
|
data-testid="version-selector"
|
|
43
|
-
className={`flex cursor-pointer items-center gap-3 p-[7px] text-sm ${
|
|
43
|
+
className={`flex cursor-pointer items-center gap-3 p-[7px] py-[5px] text-sm ${
|
|
44
44
|
darkMode
|
|
45
45
|
? "text-gray-500 hover:bg-gray-200"
|
|
46
46
|
: "text-gray-200 hover:bg-gray-500"
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
|
-
import { EditButton, useEditContext } from "../client/editContext";
|
|
3
|
+
import { EditButton, useEditContext, EditorMode } from "../client/editContext";
|
|
4
4
|
import { Rect, findComponentRect } from "../utils";
|
|
5
5
|
import { useThrottledCallback } from "use-debounce";
|
|
6
6
|
import { Component } from "../pageModel";
|
|
7
7
|
import { PageViewContext } from "../page-viewer/pageViewContext";
|
|
8
8
|
import { ArrowUpFromDot } from "lucide-react";
|
|
9
9
|
import { cn } from "../../lib/utils";
|
|
10
|
+
import { CompareIcon } from "../ui/Icons";
|
|
10
11
|
export function FrameMenu({
|
|
11
12
|
component,
|
|
12
|
-
|
|
13
|
+
compareView,
|
|
13
14
|
pageViewContext,
|
|
14
15
|
}: {
|
|
15
16
|
component: Component;
|
|
16
|
-
|
|
17
|
+
compareView: boolean;
|
|
17
18
|
pageViewContext: PageViewContext;
|
|
18
19
|
}) {
|
|
19
20
|
const editContext = useEditContext();
|
|
@@ -24,7 +25,6 @@ export function FrameMenu({
|
|
|
24
25
|
const [componentRect, setComponentRect] = useState<Rect>();
|
|
25
26
|
const [isHeaderWiderThanComponent, setIsHeaderWiderThanComponent] =
|
|
26
27
|
useState(false);
|
|
27
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
28
28
|
|
|
29
29
|
const updatePosition = () => {
|
|
30
30
|
if (!component || !editContext || !pageViewContext) return;
|
|
@@ -138,7 +138,7 @@ export function FrameMenu({
|
|
|
138
138
|
const commands = editContext.getComponentCommands([component]);
|
|
139
139
|
const isDraggable =
|
|
140
140
|
component.canBeMoved &&
|
|
141
|
-
mode === "edit" &&
|
|
141
|
+
editContext.mode === "edit" &&
|
|
142
142
|
!component.layoutId &&
|
|
143
143
|
pageViewContext.page?.item.canWriteItem;
|
|
144
144
|
false;
|
|
@@ -193,18 +193,18 @@ export function FrameMenu({
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
const isShared = component.isShared;
|
|
196
|
-
const isReadonly = mode === "
|
|
196
|
+
const isReadonly = editContext.mode === "preview" || compareView;
|
|
197
197
|
const isLayout = component.layoutId;
|
|
198
198
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
199
|
+
function getColor() {
|
|
200
|
+
if (isReadonly) return "readonly";
|
|
201
|
+
if (editContext?.mode === "suggestions") return "suggestions";
|
|
202
|
+
if (isShared) return "shared";
|
|
203
|
+
if (isLayout) return "layout";
|
|
204
|
+
if (component.canBeMoved) return "default";
|
|
205
|
+
return "nonMovable";
|
|
206
|
+
}
|
|
207
|
+
const color = getColor();
|
|
208
208
|
|
|
209
209
|
const colorVariants = {
|
|
210
210
|
shared: "border-orange-400",
|
|
@@ -212,6 +212,7 @@ export function FrameMenu({
|
|
|
212
212
|
layout: "border-purple-400",
|
|
213
213
|
default: "border-sky-400",
|
|
214
214
|
nonMovable: "border-red-400",
|
|
215
|
+
suggestions: "border-teal-400",
|
|
215
216
|
};
|
|
216
217
|
|
|
217
218
|
const bgColorVariants = {
|
|
@@ -220,6 +221,7 @@ export function FrameMenu({
|
|
|
220
221
|
layout: "bg-purple-400",
|
|
221
222
|
default: "bg-sky-400",
|
|
222
223
|
nonMovable: "bg-red-400",
|
|
224
|
+
suggestions: "bg-teal-400",
|
|
223
225
|
};
|
|
224
226
|
|
|
225
227
|
// Calculate initial estimation for the header width
|
|
@@ -241,7 +243,6 @@ export function FrameMenu({
|
|
|
241
243
|
"pointer-events-none absolute inset-0 rounded-b-sm border-2",
|
|
242
244
|
colorVariants[color],
|
|
243
245
|
"tour-frame-menu opacity-50 hover:opacity-100",
|
|
244
|
-
isHovered && "opacity-100",
|
|
245
246
|
!isMultiSelected && isHeaderWiderThanComponent && "border-t-0",
|
|
246
247
|
!isMultiSelected && !isHeaderWiderThanComponent && "rounded-tl-sm",
|
|
247
248
|
isMultiSelected && "rounded-t-sm",
|
|
@@ -296,7 +297,7 @@ export function FrameMenu({
|
|
|
296
297
|
</span>
|
|
297
298
|
)}
|
|
298
299
|
</div>
|
|
299
|
-
{mode === "edit" && (
|
|
300
|
+
{editContext.mode === "edit" && (
|
|
300
301
|
<div className="flex items-center gap-2 text-sm">
|
|
301
302
|
{buttons.map((b, i) => (
|
|
302
303
|
<div
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import { findComponent } from "../componentTreeHelper";
|
|
3
|
-
import { useEditContext } from "../client/editContext";
|
|
3
|
+
import { useEditContext, EditorMode } from "../client/editContext";
|
|
4
4
|
import { PageViewContext } from "../page-viewer/pageViewContext";
|
|
5
5
|
import { FrameMenu } from "./FrameMenu";
|
|
6
6
|
import { Component } from "../pageModel";
|
|
7
7
|
export function FrameMenus({
|
|
8
|
-
|
|
8
|
+
compareView,
|
|
9
9
|
pageViewContext,
|
|
10
10
|
}: {
|
|
11
|
-
|
|
11
|
+
compareView: boolean;
|
|
12
12
|
pageViewContext: PageViewContext;
|
|
13
13
|
}) {
|
|
14
14
|
const editContext = useEditContext();
|
|
@@ -24,8 +24,8 @@ export function FrameMenus({
|
|
|
24
24
|
.map((id) =>
|
|
25
25
|
findComponent(
|
|
26
26
|
id,
|
|
27
|
-
pageViewContext.page?.rootComponent.placeholders || []
|
|
28
|
-
)
|
|
27
|
+
pageViewContext.page?.rootComponent.placeholders || [],
|
|
28
|
+
),
|
|
29
29
|
)
|
|
30
30
|
.filter((c): c is NonNullable<typeof c> => c !== undefined)
|
|
31
31
|
: [];
|
|
@@ -39,7 +39,7 @@ export function FrameMenus({
|
|
|
39
39
|
<FrameMenu
|
|
40
40
|
key={c.id}
|
|
41
41
|
component={c}
|
|
42
|
-
|
|
42
|
+
compareView={compareView}
|
|
43
43
|
pageViewContext={pageViewContext}
|
|
44
44
|
/>
|
|
45
45
|
))}{" "}
|
|
@@ -2,19 +2,23 @@ import { useEffect, useRef } from "react";
|
|
|
2
2
|
import {
|
|
3
3
|
useEditContext,
|
|
4
4
|
useModifiedFieldsContext,
|
|
5
|
+
useEditContextRef,
|
|
5
6
|
} from "../client/editContext";
|
|
6
7
|
import { useThrottledCallback } from "use-debounce";
|
|
7
8
|
import { getFieldDescriptorFromElement, hasFieldLock } from "../utils";
|
|
8
9
|
import { PageViewContext } from "../page-viewer/pageViewContext";
|
|
10
|
+
import { applyPatch, convertChangesToXML, diffWords } from "diff";
|
|
11
|
+
import { createPatch } from "diff";
|
|
9
12
|
|
|
10
13
|
export function InlineEditor({
|
|
11
14
|
pageViewContext,
|
|
12
|
-
|
|
15
|
+
compareView,
|
|
13
16
|
}: {
|
|
14
17
|
pageViewContext: PageViewContext;
|
|
15
|
-
|
|
18
|
+
compareView: boolean;
|
|
16
19
|
}) {
|
|
17
20
|
const context = useEditContext();
|
|
21
|
+
const contextRef = useEditContextRef();
|
|
18
22
|
const modifiedFieldsContext = useModifiedFieldsContext();
|
|
19
23
|
|
|
20
24
|
if (!context) return;
|
|
@@ -33,7 +37,7 @@ export function InlineEditor({
|
|
|
33
37
|
|
|
34
38
|
if (modifiedFieldValue === value) return;
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
contextRef.current?.operations.editField({
|
|
37
41
|
field: {
|
|
38
42
|
fieldId,
|
|
39
43
|
fieldName: fieldName ?? undefined,
|
|
@@ -51,7 +55,7 @@ export function InlineEditor({
|
|
|
51
55
|
);
|
|
52
56
|
|
|
53
57
|
useEffect(() => {
|
|
54
|
-
if (!context || mode === "
|
|
58
|
+
if (!context || compareView || context.mode === "preview") return;
|
|
55
59
|
const element = context.inlineEditingFieldElement;
|
|
56
60
|
|
|
57
61
|
const editableElements =
|
|
@@ -111,37 +115,244 @@ export function InlineEditor({
|
|
|
111
115
|
};
|
|
112
116
|
}, [context?.inlineEditingFieldElement]);
|
|
113
117
|
|
|
118
|
+
function saveCaretPosition(editableElement: HTMLElement): Range | null {
|
|
119
|
+
const selection =
|
|
120
|
+
pageViewContext.editorIframeRef.current?.contentWindow?.getSelection();
|
|
121
|
+
if (
|
|
122
|
+
selection &&
|
|
123
|
+
selection.rangeCount > 0 &&
|
|
124
|
+
editableElement.contains(selection.anchorNode)
|
|
125
|
+
) {
|
|
126
|
+
return selection.getRangeAt(0);
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function restoreCaretPosition(range: Range | null) {
|
|
132
|
+
if (range) {
|
|
133
|
+
const selection =
|
|
134
|
+
pageViewContext.editorIframeRef.current?.contentWindow?.getSelection();
|
|
135
|
+
selection?.removeAllRanges();
|
|
136
|
+
selection?.addRange(range);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
114
140
|
useEffect(() => {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
141
|
+
const updateFieldsWithSuggestions = async () => {
|
|
142
|
+
const iframeWindow =
|
|
143
|
+
pageViewContext.editorIframeRef.current?.contentWindow;
|
|
144
|
+
if (!iframeWindow) return;
|
|
145
|
+
const doc = iframeWindow.document;
|
|
146
|
+
|
|
147
|
+
// Query all field elements by data attributes.
|
|
148
|
+
const fieldElements = doc.querySelectorAll(
|
|
149
|
+
"[data-fieldid][data-itemid][data-language][data-version]",
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
fieldElements.forEach(async (element) => {
|
|
153
|
+
// Do not update if this field is currently focused.
|
|
154
|
+
if (element === context.inlineEditingFieldElement) return;
|
|
155
|
+
|
|
156
|
+
const fieldId = element.getAttribute("data-fieldid");
|
|
157
|
+
const itemId = element.getAttribute("data-itemid");
|
|
158
|
+
const language = element.getAttribute("data-language");
|
|
159
|
+
const versionStr = element.getAttribute("data-version");
|
|
160
|
+
if (!fieldId || !itemId || !language || !versionStr) return;
|
|
161
|
+
const version = parseInt(versionStr, 10);
|
|
162
|
+
|
|
163
|
+
// Build an item descriptor.
|
|
164
|
+
const descriptor = { id: itemId, language, version };
|
|
165
|
+
|
|
166
|
+
// Fetch the current item from the repository.
|
|
167
|
+
const loadedItem = await context.itemsRepository.getItem(descriptor);
|
|
168
|
+
if (!loadedItem) return;
|
|
169
|
+
|
|
170
|
+
// Get the baseline from repository.
|
|
171
|
+
const repositoryField = loadedItem.fields.find(
|
|
172
|
+
(f: any) => f.id === fieldId,
|
|
173
|
+
);
|
|
174
|
+
let originalValue = repositoryField
|
|
175
|
+
? repositoryField.rawValue || ""
|
|
176
|
+
: "";
|
|
177
|
+
|
|
178
|
+
// If the field is modified locally, use that value.
|
|
179
|
+
const modField = modifiedFieldsContext?.modifiedFields.find(
|
|
180
|
+
(mod: any) =>
|
|
181
|
+
mod.fieldId === fieldId &&
|
|
182
|
+
mod.item.id === itemId &&
|
|
183
|
+
mod.item.language === language &&
|
|
184
|
+
mod.item.version === version,
|
|
185
|
+
);
|
|
186
|
+
if (modField) {
|
|
187
|
+
originalValue = modField.value || "";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// If showSuggestedEdits is false, update with the base value.
|
|
191
|
+
if (!context.showSuggestedEdits) {
|
|
192
|
+
element.innerHTML = originalValue;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Otherwise, gather all suggestions for this field.
|
|
197
|
+
const fieldSuggestions = context.suggestedEdits.filter(
|
|
198
|
+
(s: any) =>
|
|
199
|
+
s.fieldId === fieldId &&
|
|
200
|
+
s.itemId === itemId &&
|
|
201
|
+
s.mainItemLanguage === language &&
|
|
202
|
+
s.mainItemId === pageViewContext.pageItemDescriptor?.id &&
|
|
203
|
+
s.mainItemVersion === version,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Sort suggestions in chronological order (oldest first).
|
|
207
|
+
fieldSuggestions.sort(
|
|
208
|
+
(a: any, b: any) =>
|
|
209
|
+
new Date(a.created).getTime() - new Date(b.created).getTime(),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
// Apply suggestions sequentially to generate the merged value.
|
|
213
|
+
let mergedValue = originalValue;
|
|
214
|
+
for (const suggestion of fieldSuggestions) {
|
|
215
|
+
// Compute a patch from the suggestion's baseline to its intended new value.
|
|
216
|
+
const patch = createPatch(
|
|
217
|
+
"field",
|
|
218
|
+
suggestion.oldValue,
|
|
219
|
+
suggestion.newValue,
|
|
120
220
|
);
|
|
221
|
+
const patchedCandidate = applyPatch(mergedValue, patch);
|
|
222
|
+
if (
|
|
223
|
+
patchedCandidate !== false &&
|
|
224
|
+
typeof patchedCandidate === "string"
|
|
225
|
+
) {
|
|
226
|
+
mergedValue = patchedCandidate;
|
|
227
|
+
}
|
|
228
|
+
// If a patch fails, we simply skip that suggestion.
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// If showSuggestedEditsDiff is false, show only the merged text
|
|
232
|
+
if (!context.showSuggestedEditsDiff) {
|
|
233
|
+
element.innerHTML = mergedValue;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
121
236
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
237
|
+
// Compute a word-based diff between originalValue and mergedValue.
|
|
238
|
+
const diffParts = diffWords(originalValue, mergedValue);
|
|
239
|
+
|
|
240
|
+
// Build HTML markup from the diff:
|
|
241
|
+
let diffHTML = "";
|
|
242
|
+
diffParts.forEach((part: any) => {
|
|
243
|
+
let style = "";
|
|
244
|
+
if (part.added) {
|
|
245
|
+
style = "color: green;";
|
|
246
|
+
} else if (part.removed) {
|
|
247
|
+
style = "color: red; text-decoration: line-through;";
|
|
248
|
+
} else {
|
|
249
|
+
style = "color: gray;";
|
|
134
250
|
}
|
|
251
|
+
// Escape any HTML in part.value if needed.
|
|
252
|
+
diffHTML += `<span style="${style}">${part.value}</span>`;
|
|
135
253
|
});
|
|
254
|
+
|
|
255
|
+
// Update the element's innerHTML with the diff markup.
|
|
256
|
+
element.innerHTML = diffHTML;
|
|
136
257
|
});
|
|
137
258
|
};
|
|
138
259
|
|
|
139
|
-
|
|
260
|
+
updateFieldsWithSuggestions();
|
|
140
261
|
}, [
|
|
141
262
|
modifiedFieldsContext?.modifiedFields,
|
|
142
263
|
context?.itemsRepository.revision,
|
|
143
264
|
context?.inlineEditingFieldElement,
|
|
265
|
+
context?.showSuggestedEdits,
|
|
266
|
+
context?.suggestedEdits,
|
|
267
|
+
pageViewContext.pageItemDescriptor,
|
|
268
|
+
context.pageView.page,
|
|
269
|
+
context.showSuggestedEditsDiff,
|
|
144
270
|
]);
|
|
145
271
|
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
async function updateFocusedFieldContent() {
|
|
274
|
+
const element = context?.inlineEditingFieldElement;
|
|
275
|
+
if (!element) return;
|
|
276
|
+
|
|
277
|
+
const savedRange = saveCaretPosition(element);
|
|
278
|
+
console.log("savedRange", savedRange, element);
|
|
279
|
+
|
|
280
|
+
const fieldId = element.getAttribute("data-fieldid");
|
|
281
|
+
const itemId = element.getAttribute("data-itemid");
|
|
282
|
+
const language = element.getAttribute("data-language");
|
|
283
|
+
const versionStr = element.getAttribute("data-version");
|
|
284
|
+
if (!fieldId || !itemId || !language || !versionStr) return;
|
|
285
|
+
|
|
286
|
+
const version = parseInt(versionStr, 10);
|
|
287
|
+
const descriptor = { id: itemId, language, version };
|
|
288
|
+
|
|
289
|
+
// Retrieve the current field value from the repository.
|
|
290
|
+
const loadedItem = await context.itemsRepository.getItem(descriptor);
|
|
291
|
+
if (!loadedItem) return;
|
|
292
|
+
// Get the baseline value from the repository.
|
|
293
|
+
const repositoryField = loadedItem.fields.find(
|
|
294
|
+
(f: any) => f.id === fieldId,
|
|
295
|
+
);
|
|
296
|
+
let baseValue = repositoryField ? repositoryField.rawValue || "" : "";
|
|
297
|
+
|
|
298
|
+
// Override with the modified field value if it exists.
|
|
299
|
+
const modField = modifiedFieldsContext?.modifiedFields.find(
|
|
300
|
+
(mod: any) =>
|
|
301
|
+
mod.fieldId === fieldId &&
|
|
302
|
+
mod.item.id === itemId &&
|
|
303
|
+
mod.item.language === language &&
|
|
304
|
+
mod.item.version === version,
|
|
305
|
+
);
|
|
306
|
+
if (modField) {
|
|
307
|
+
baseValue = modField.value || "";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// If suggestions mode is active, merge all suggestions for this field.
|
|
311
|
+
|
|
312
|
+
if (context.showSuggestedEdits || context.mode === "suggestions") {
|
|
313
|
+
const fieldSuggestions = context.suggestedEdits.filter(
|
|
314
|
+
(s: any) =>
|
|
315
|
+
s.fieldId === fieldId &&
|
|
316
|
+
s.itemId === itemId &&
|
|
317
|
+
s.mainItemLanguage === language &&
|
|
318
|
+
s.mainItemVersion === version,
|
|
319
|
+
);
|
|
320
|
+
// Sort suggestions in chronological order (oldest first).
|
|
321
|
+
fieldSuggestions.sort(
|
|
322
|
+
(a: any, b: any) =>
|
|
323
|
+
new Date(a.created).getTime() - new Date(b.created).getTime(),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
let mergedValue = baseValue;
|
|
327
|
+
for (const suggestion of fieldSuggestions) {
|
|
328
|
+
const patch = createPatch(
|
|
329
|
+
"field",
|
|
330
|
+
suggestion.oldValue,
|
|
331
|
+
suggestion.newValue,
|
|
332
|
+
);
|
|
333
|
+
const patchedCandidate = applyPatch(mergedValue, patch);
|
|
334
|
+
if (
|
|
335
|
+
patchedCandidate !== false &&
|
|
336
|
+
typeof patchedCandidate === "string"
|
|
337
|
+
) {
|
|
338
|
+
mergedValue = patchedCandidate;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Update focused field element with the merged plain text value.
|
|
342
|
+
element.innerHTML = mergedValue;
|
|
343
|
+
} else {
|
|
344
|
+
if (element.innerHTML !== baseValue) {
|
|
345
|
+
// If suggestions mode is off, update with the base value.
|
|
346
|
+
element.innerHTML = baseValue;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
console.log("restoring caret position", savedRange);
|
|
351
|
+
restoreCaretPosition(savedRange);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
updateFocusedFieldContent();
|
|
355
|
+
}, [context?.inlineEditingFieldElement]);
|
|
356
|
+
|
|
146
357
|
return null;
|
|
147
358
|
}
|