@alpaca-editor/core 1.0.4041 → 1.0.4042
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/ui/textarea.d.ts +1 -1
- package/dist/components/ui/textarea.js +5 -3
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/editor/commands/componentCommands.js +18 -5
- package/dist/editor/commands/componentCommands.js.map +1 -1
- package/dist/editor/page-editor-chrome/CommentHighlighting.js +30 -10
- package/dist/editor/page-editor-chrome/CommentHighlighting.js.map +1 -1
- package/dist/editor/page-editor-chrome/SuggestionHighlighting.js +17 -13
- package/dist/editor/page-editor-chrome/SuggestionHighlighting.js.map +1 -1
- package/dist/editor/reviews/CommentDisplayPopover.d.ts +9 -0
- package/dist/editor/reviews/CommentDisplayPopover.js +101 -0
- package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -0
- package/dist/editor/reviews/CommentPopover.d.ts +15 -0
- package/dist/editor/reviews/CommentPopover.js +151 -0
- package/dist/editor/reviews/CommentPopover.js.map +1 -0
- package/dist/editor/reviews/SuggestionDisplayPopover.d.ts +9 -0
- package/dist/editor/reviews/SuggestionDisplayPopover.js +186 -0
- package/dist/editor/reviews/SuggestionDisplayPopover.js.map +1 -0
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/styles.css +3 -0
- package/package.json +1 -1
- package/src/components/ui/textarea.tsx +7 -2
- package/src/editor/commands/componentCommands.tsx +16 -4
- package/src/editor/page-editor-chrome/CommentHighlighting.tsx +49 -18
- package/src/editor/page-editor-chrome/SuggestionHighlighting.tsx +36 -26
- package/src/editor/reviews/CommentDisplayPopover.tsx +326 -0
- package/src/editor/reviews/CommentPopover.tsx +249 -0
- package/src/editor/reviews/SuggestionDisplayPopover.tsx +410 -0
- package/src/revision.ts +2 -2
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { SuggestedEdit } from "../../types";
|
|
6
|
+
import { useEditContext } from "../client/editContext";
|
|
7
|
+
import {
|
|
8
|
+
Popover,
|
|
9
|
+
PopoverContent,
|
|
10
|
+
PopoverTrigger,
|
|
11
|
+
} from "../../components/ui/popover";
|
|
12
|
+
import { formatDate } from "../utils";
|
|
13
|
+
import { Button } from "../../components/ui/button";
|
|
14
|
+
import {
|
|
15
|
+
deleteSuggestedEdit,
|
|
16
|
+
createOrUpdateSuggestedEdit,
|
|
17
|
+
} from "../services/suggestedEditsService";
|
|
18
|
+
import {
|
|
19
|
+
Check,
|
|
20
|
+
Trash2,
|
|
21
|
+
UserRoundPen,
|
|
22
|
+
Brush,
|
|
23
|
+
GalleryVertical,
|
|
24
|
+
} from "lucide-react";
|
|
25
|
+
import { DiffView } from "./DiffView";
|
|
26
|
+
import { SimpleIconButton } from "../ui/SimpleIconButton";
|
|
27
|
+
import { createPatch, applyPatch } from "diff";
|
|
28
|
+
import { cn } from "../../lib/utils";
|
|
29
|
+
|
|
30
|
+
interface SuggestionDisplayPopoverProps {
|
|
31
|
+
suggestion: SuggestedEdit;
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
onSuggestionUpdated?: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function SuggestionDisplayPopover({
|
|
37
|
+
suggestion,
|
|
38
|
+
children,
|
|
39
|
+
onSuggestionUpdated,
|
|
40
|
+
}: SuggestionDisplayPopoverProps) {
|
|
41
|
+
const editContext = useEditContext();
|
|
42
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
43
|
+
const [deleteConfirm, setDeleteConfirm] = useState(false);
|
|
44
|
+
const [isApplying, setIsApplying] = useState(false);
|
|
45
|
+
const [item, setItem] = useState<any>(null);
|
|
46
|
+
const [patchPossible, setPatchPossible] = useState<boolean>(true);
|
|
47
|
+
const [patchWarning, setPatchWarning] = useState<string>("");
|
|
48
|
+
const [ignoreFormatting, setIgnoreFormatting] = useState(true);
|
|
49
|
+
const [clipUnchanged, setClipUnchanged] = useState(true);
|
|
50
|
+
|
|
51
|
+
const canDelete =
|
|
52
|
+
suggestion.author === editContext?.user?.name &&
|
|
53
|
+
suggestion.status !== "applied";
|
|
54
|
+
|
|
55
|
+
const canApply =
|
|
56
|
+
editContext?.mode === "edit" &&
|
|
57
|
+
suggestion.status !== "applied" &&
|
|
58
|
+
!editContext?.readonly;
|
|
59
|
+
|
|
60
|
+
// Load the full item when popover opens
|
|
61
|
+
React.useEffect(() => {
|
|
62
|
+
if (isOpen && editContext?.itemsRepository && suggestion.itemId) {
|
|
63
|
+
editContext.itemsRepository
|
|
64
|
+
.getItem({
|
|
65
|
+
id: suggestion.itemId,
|
|
66
|
+
language: suggestion.mainItemLanguage,
|
|
67
|
+
version: suggestion.mainItemVersion,
|
|
68
|
+
})
|
|
69
|
+
.then((loadedItem) => {
|
|
70
|
+
setItem(loadedItem);
|
|
71
|
+
checkAndComputePatch(loadedItem);
|
|
72
|
+
})
|
|
73
|
+
.catch((err) => {
|
|
74
|
+
console.error("Error loading item:", err);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}, [isOpen, suggestion, editContext?.itemsRepository]);
|
|
78
|
+
|
|
79
|
+
// Check if the patch can be applied cleanly
|
|
80
|
+
const checkAndComputePatch = async (loadedItem: any) => {
|
|
81
|
+
const field = loadedItem?.fields?.find(
|
|
82
|
+
(f: { id: string }) => f.id === suggestion.fieldId,
|
|
83
|
+
);
|
|
84
|
+
if (!field) return;
|
|
85
|
+
|
|
86
|
+
const currentValue: string = field.rawValue || "";
|
|
87
|
+
const patch = createPatch(
|
|
88
|
+
"field",
|
|
89
|
+
suggestion.oldValue,
|
|
90
|
+
suggestion.newValue,
|
|
91
|
+
);
|
|
92
|
+
const patchedCandidate = applyPatch(currentValue, patch);
|
|
93
|
+
|
|
94
|
+
if (patchedCandidate === false || typeof patchedCandidate !== "string") {
|
|
95
|
+
setPatchPossible(false);
|
|
96
|
+
setPatchWarning(
|
|
97
|
+
"Patch cannot be applied cleanly to current field value.",
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
setPatchPossible(true);
|
|
101
|
+
setPatchWarning("");
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const handleDelete = async () => {
|
|
106
|
+
if (!deleteConfirm) {
|
|
107
|
+
setDeleteConfirm(true);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await deleteSuggestedEdit(suggestion);
|
|
112
|
+
setIsOpen(false);
|
|
113
|
+
onSuggestionUpdated?.();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const handleApplyPatch = async () => {
|
|
117
|
+
if (!patchPossible || !editContext || isApplying) return;
|
|
118
|
+
|
|
119
|
+
setIsApplying(true);
|
|
120
|
+
try {
|
|
121
|
+
// Recalculate the patch immediately before applying
|
|
122
|
+
const field = item?.fields?.find(
|
|
123
|
+
(f: { id: string }) => f.id === suggestion.fieldId,
|
|
124
|
+
);
|
|
125
|
+
if (!field) return;
|
|
126
|
+
|
|
127
|
+
const currentValue: string = field.rawValue || "";
|
|
128
|
+
const patch = createPatch(
|
|
129
|
+
"field",
|
|
130
|
+
suggestion.oldValue,
|
|
131
|
+
suggestion.newValue,
|
|
132
|
+
);
|
|
133
|
+
const patchedCandidate = applyPatch(currentValue, patch);
|
|
134
|
+
|
|
135
|
+
if (patchedCandidate === false || typeof patchedCandidate !== "string") {
|
|
136
|
+
setPatchWarning(
|
|
137
|
+
"Patch cannot be applied cleanly to current field value.",
|
|
138
|
+
);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
await editContext.operations.editField({
|
|
143
|
+
field: {
|
|
144
|
+
fieldId: suggestion.fieldId,
|
|
145
|
+
item: {
|
|
146
|
+
id: suggestion.itemId,
|
|
147
|
+
language: suggestion.mainItemLanguage,
|
|
148
|
+
version: suggestion.mainItemVersion,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
value: patchedCandidate,
|
|
152
|
+
rawValue: patchedCandidate,
|
|
153
|
+
refresh: "immediate",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Update the suggestion status to "applied"
|
|
157
|
+
const updatedSuggestion = { ...suggestion, status: "applied" as const };
|
|
158
|
+
await createOrUpdateSuggestedEdit(updatedSuggestion);
|
|
159
|
+
onSuggestionUpdated?.();
|
|
160
|
+
setIsOpen(false);
|
|
161
|
+
} finally {
|
|
162
|
+
setIsApplying(false);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const handleReplaceCompletely = async () => {
|
|
167
|
+
if (!editContext || isApplying) return;
|
|
168
|
+
|
|
169
|
+
setIsApplying(true);
|
|
170
|
+
try {
|
|
171
|
+
await editContext.operations.editField({
|
|
172
|
+
field: {
|
|
173
|
+
fieldId: suggestion.fieldId,
|
|
174
|
+
item: {
|
|
175
|
+
id: suggestion.itemId,
|
|
176
|
+
language: suggestion.mainItemLanguage,
|
|
177
|
+
version: suggestion.mainItemVersion,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
value: suggestion.newValue,
|
|
181
|
+
rawValue: suggestion.newValue,
|
|
182
|
+
refresh: "immediate",
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const updatedSuggestion = { ...suggestion, status: "applied" as const };
|
|
186
|
+
await createOrUpdateSuggestedEdit(updatedSuggestion);
|
|
187
|
+
onSuggestionUpdated?.();
|
|
188
|
+
setIsOpen(false);
|
|
189
|
+
} finally {
|
|
190
|
+
setIsApplying(false);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const renderContextInfo = () => {
|
|
195
|
+
const itemName = item ? item.name : null;
|
|
196
|
+
const fieldName =
|
|
197
|
+
item && item.fields
|
|
198
|
+
? item.fields.find(
|
|
199
|
+
(f: { id: string; name?: string }) => f.id === suggestion.fieldId,
|
|
200
|
+
)?.name
|
|
201
|
+
: null;
|
|
202
|
+
|
|
203
|
+
if (!itemName && !fieldName) return null;
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<div className="mt-3 flex items-center border-t pt-3 text-xs">
|
|
207
|
+
{itemName && <div className="text-xs text-gray-500">{itemName}</div>}
|
|
208
|
+
{fieldName && itemName && (
|
|
209
|
+
<div className="mx-2 text-xs text-gray-500">></div>
|
|
210
|
+
)}
|
|
211
|
+
{fieldName && <div className="text-xs text-gray-500">{fieldName}</div>}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const renderDiffToggleButtons = () => {
|
|
217
|
+
return (
|
|
218
|
+
<div className="mb-2 flex gap-2">
|
|
219
|
+
<SimpleIconButton
|
|
220
|
+
icon={<Brush size={14} className="p-0.5" strokeWidth={1} />}
|
|
221
|
+
label="Ignore Formatting"
|
|
222
|
+
onClick={() => setIgnoreFormatting((prev) => !prev)}
|
|
223
|
+
className={cn("text-gray-500", ignoreFormatting ? "bg-gray-200" : "")}
|
|
224
|
+
/>
|
|
225
|
+
<SimpleIconButton
|
|
226
|
+
icon={<GalleryVertical size={14} className="p-0.5" strokeWidth={1} />}
|
|
227
|
+
label="Clip"
|
|
228
|
+
onClick={() => setClipUnchanged((prev) => !prev)}
|
|
229
|
+
className={cn("text-gray-500", clipUnchanged ? "bg-gray-200" : "")}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<Popover
|
|
237
|
+
open={isOpen}
|
|
238
|
+
onOpenChange={(open) => {
|
|
239
|
+
setIsOpen(open);
|
|
240
|
+
if (!open) {
|
|
241
|
+
setDeleteConfirm(false);
|
|
242
|
+
setPatchWarning("");
|
|
243
|
+
}
|
|
244
|
+
}}
|
|
245
|
+
enableIframeClickDetection={true}
|
|
246
|
+
>
|
|
247
|
+
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
|
248
|
+
<PopoverContent className="w-96 p-4" side="bottom" align="start">
|
|
249
|
+
<div className="space-y-3">
|
|
250
|
+
{/* Header */}
|
|
251
|
+
<div className="flex items-start justify-between">
|
|
252
|
+
<div>
|
|
253
|
+
<div className="text-sm font-semibold text-gray-900">
|
|
254
|
+
{suggestion.authorDisplayName || suggestion.author}
|
|
255
|
+
</div>
|
|
256
|
+
<div className="text-xs text-gray-500">
|
|
257
|
+
{suggestion.created
|
|
258
|
+
? formatDate(new Date(suggestion.created))
|
|
259
|
+
: ""}
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div className="flex items-center gap-1">
|
|
263
|
+
{canApply && patchPossible && suggestion.status !== "applied" && (
|
|
264
|
+
<Button
|
|
265
|
+
variant="ghost"
|
|
266
|
+
size="sm"
|
|
267
|
+
onClick={handleApplyPatch}
|
|
268
|
+
disabled={isApplying}
|
|
269
|
+
className="h-8 w-8 p-0 text-green-600"
|
|
270
|
+
>
|
|
271
|
+
<Check size={14} strokeWidth={1} />
|
|
272
|
+
</Button>
|
|
273
|
+
)}
|
|
274
|
+
{suggestion.status === "applied" && (
|
|
275
|
+
<div className="flex h-8 w-8 items-center justify-center">
|
|
276
|
+
<Check size={14} strokeWidth={1} className="text-green-500" />
|
|
277
|
+
</div>
|
|
278
|
+
)}
|
|
279
|
+
{canDelete && (
|
|
280
|
+
<Button
|
|
281
|
+
variant="ghost"
|
|
282
|
+
size="sm"
|
|
283
|
+
onClick={handleDelete}
|
|
284
|
+
className={`h-8 w-8 p-0 ${deleteConfirm ? "text-red-500" : "text-gray-400"}`}
|
|
285
|
+
>
|
|
286
|
+
<Trash2 size={14} strokeWidth={1} />
|
|
287
|
+
</Button>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
{/* Status */}
|
|
293
|
+
{suggestion.status === "applied" && (
|
|
294
|
+
<div className="text-xs font-medium text-green-600">
|
|
295
|
+
✓ Applied{" "}
|
|
296
|
+
{suggestion.updatedBy ? `by ${suggestion.updatedBy}` : ""}
|
|
297
|
+
{suggestion.updated
|
|
298
|
+
? ` on ${formatDate(new Date(suggestion.updated))}`
|
|
299
|
+
: ""}
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
302
|
+
|
|
303
|
+
{/* Diff Toggle Buttons */}
|
|
304
|
+
{renderDiffToggleButtons()}
|
|
305
|
+
|
|
306
|
+
{/* Diff View */}
|
|
307
|
+
<div className="text-sm">
|
|
308
|
+
<DiffView
|
|
309
|
+
oldText={suggestion.oldValue}
|
|
310
|
+
newText={suggestion.newValue}
|
|
311
|
+
ignoreFormatting={ignoreFormatting}
|
|
312
|
+
clipUnchanged={clipUnchanged}
|
|
313
|
+
clipThreshold={50}
|
|
314
|
+
clipContext={10}
|
|
315
|
+
/>
|
|
316
|
+
{canApply && patchWarning && (
|
|
317
|
+
<div className="mt-2 text-xs text-red-500">
|
|
318
|
+
{patchWarning}
|
|
319
|
+
<button
|
|
320
|
+
className="ml-2 cursor-pointer underline hover:text-red-700"
|
|
321
|
+
onClick={handleReplaceCompletely}
|
|
322
|
+
disabled={isApplying}
|
|
323
|
+
>
|
|
324
|
+
Click here to replace the field content completely.
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
{/* Context Info */}
|
|
331
|
+
{renderContextInfo()}
|
|
332
|
+
|
|
333
|
+
{/* Delete confirmation */}
|
|
334
|
+
{deleteConfirm && (
|
|
335
|
+
<div className="border-t pt-3">
|
|
336
|
+
<div className="mb-2 text-sm text-red-600">
|
|
337
|
+
Are you sure you want to delete this suggestion?
|
|
338
|
+
</div>
|
|
339
|
+
<div className="flex gap-2">
|
|
340
|
+
<Button
|
|
341
|
+
variant="outline"
|
|
342
|
+
size="sm"
|
|
343
|
+
onClick={() => setDeleteConfirm(false)}
|
|
344
|
+
>
|
|
345
|
+
Cancel
|
|
346
|
+
</Button>
|
|
347
|
+
<Button variant="destructive" size="sm" onClick={handleDelete}>
|
|
348
|
+
Delete
|
|
349
|
+
</Button>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{/* Apply button when patch is not possible */}
|
|
355
|
+
{canApply && !patchPossible && suggestion.status !== "applied" && (
|
|
356
|
+
<div className="border-t pt-3">
|
|
357
|
+
<Button
|
|
358
|
+
size="sm"
|
|
359
|
+
onClick={handleReplaceCompletely}
|
|
360
|
+
disabled={isApplying}
|
|
361
|
+
className="w-full"
|
|
362
|
+
>
|
|
363
|
+
{isApplying ? "Applying..." : "Replace Field Content"}
|
|
364
|
+
</Button>
|
|
365
|
+
</div>
|
|
366
|
+
)}
|
|
367
|
+
|
|
368
|
+
{/* Actions */}
|
|
369
|
+
{!deleteConfirm && (
|
|
370
|
+
<div className="border-t pt-3">
|
|
371
|
+
<Button
|
|
372
|
+
variant="outline"
|
|
373
|
+
size="sm"
|
|
374
|
+
onClick={() => {
|
|
375
|
+
setIsOpen(false);
|
|
376
|
+
// Exit fullscreen mode if active
|
|
377
|
+
if (editContext?.pageView?.fullscreen) {
|
|
378
|
+
editContext.pageView.setFullscreen(false);
|
|
379
|
+
}
|
|
380
|
+
// Focus on the field and select the item
|
|
381
|
+
if (suggestion.fieldId) {
|
|
382
|
+
editContext?.setFocusedField(
|
|
383
|
+
{
|
|
384
|
+
fieldId: suggestion.fieldId,
|
|
385
|
+
item: {
|
|
386
|
+
id: suggestion.itemId,
|
|
387
|
+
language: suggestion.mainItemLanguage,
|
|
388
|
+
version: suggestion.mainItemVersion,
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
false,
|
|
392
|
+
);
|
|
393
|
+
editContext?.select?.([suggestion.itemId]);
|
|
394
|
+
}
|
|
395
|
+
// Switch to comments view
|
|
396
|
+
if (editContext?.switchView) {
|
|
397
|
+
editContext.switchView("comments");
|
|
398
|
+
}
|
|
399
|
+
}}
|
|
400
|
+
className="w-full"
|
|
401
|
+
>
|
|
402
|
+
View in Comments Panel
|
|
403
|
+
</Button>
|
|
404
|
+
</div>
|
|
405
|
+
)}
|
|
406
|
+
</div>
|
|
407
|
+
</PopoverContent>
|
|
408
|
+
</Popover>
|
|
409
|
+
);
|
|
410
|
+
}
|
package/src/revision.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const version = "1.0.
|
|
2
|
-
export const buildDate = "2025-08-13
|
|
1
|
+
export const version = "1.0.4042";
|
|
2
|
+
export const buildDate = "2025-08-13 09:49:23";
|