@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.
Files changed (30) hide show
  1. package/dist/components/ui/textarea.d.ts +1 -1
  2. package/dist/components/ui/textarea.js +5 -3
  3. package/dist/components/ui/textarea.js.map +1 -1
  4. package/dist/editor/commands/componentCommands.js +18 -5
  5. package/dist/editor/commands/componentCommands.js.map +1 -1
  6. package/dist/editor/page-editor-chrome/CommentHighlighting.js +30 -10
  7. package/dist/editor/page-editor-chrome/CommentHighlighting.js.map +1 -1
  8. package/dist/editor/page-editor-chrome/SuggestionHighlighting.js +17 -13
  9. package/dist/editor/page-editor-chrome/SuggestionHighlighting.js.map +1 -1
  10. package/dist/editor/reviews/CommentDisplayPopover.d.ts +9 -0
  11. package/dist/editor/reviews/CommentDisplayPopover.js +101 -0
  12. package/dist/editor/reviews/CommentDisplayPopover.js.map +1 -0
  13. package/dist/editor/reviews/CommentPopover.d.ts +15 -0
  14. package/dist/editor/reviews/CommentPopover.js +151 -0
  15. package/dist/editor/reviews/CommentPopover.js.map +1 -0
  16. package/dist/editor/reviews/SuggestionDisplayPopover.d.ts +9 -0
  17. package/dist/editor/reviews/SuggestionDisplayPopover.js +186 -0
  18. package/dist/editor/reviews/SuggestionDisplayPopover.js.map +1 -0
  19. package/dist/revision.d.ts +2 -2
  20. package/dist/revision.js +2 -2
  21. package/dist/styles.css +3 -0
  22. package/package.json +1 -1
  23. package/src/components/ui/textarea.tsx +7 -2
  24. package/src/editor/commands/componentCommands.tsx +16 -4
  25. package/src/editor/page-editor-chrome/CommentHighlighting.tsx +49 -18
  26. package/src/editor/page-editor-chrome/SuggestionHighlighting.tsx +36 -26
  27. package/src/editor/reviews/CommentDisplayPopover.tsx +326 -0
  28. package/src/editor/reviews/CommentPopover.tsx +249 -0
  29. package/src/editor/reviews/SuggestionDisplayPopover.tsx +410 -0
  30. 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">&gt;</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.4041";
2
- export const buildDate = "2025-08-13 02:12:57";
1
+ export const version = "1.0.4042";
2
+ export const buildDate = "2025-08-13 09:49:23";