@alpaca-editor/core 1.0.4105 → 1.0.4106
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 +7 -0
- package/dist/config/config.js.map +1 -1
- package/dist/editor/control-center/IndexOverview.js +48 -30
- package/dist/editor/control-center/IndexOverview.js.map +1 -1
- package/dist/editor/control-center/LatestFeedback.d.ts +1 -0
- package/dist/editor/control-center/LatestFeedback.js +134 -0
- package/dist/editor/control-center/LatestFeedback.js.map +1 -0
- package/dist/editor/field-types/richtext/contextMenuFactory.js +11 -0
- package/dist/editor/field-types/richtext/contextMenuFactory.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/EditControls.js +1 -2
- package/dist/editor/menubar/toolbar-sections/EditControls.js.map +1 -1
- package/dist/editor/menubar/toolbar-sections/ViewportControls.js +3 -2
- package/dist/editor/menubar/toolbar-sections/ViewportControls.js.map +1 -1
- package/dist/editor/reviews/Comment.js +12 -2
- package/dist/editor/reviews/Comment.js.map +1 -1
- package/dist/editor/reviews/CommentView.d.ts +3 -1
- package/dist/editor/reviews/CommentView.js +9 -6
- package/dist/editor/reviews/CommentView.js.map +1 -1
- package/dist/editor/reviews/Comments.js +21 -94
- package/dist/editor/reviews/Comments.js.map +1 -1
- package/dist/editor/reviews/SuggestedEdit.js +8 -6
- package/dist/editor/reviews/SuggestedEdit.js.map +1 -1
- package/dist/editor/reviews/SuggestionDisplayPopover.js +7 -5
- package/dist/editor/reviews/SuggestionDisplayPopover.js.map +1 -1
- package/dist/editor/services/reviewsService.d.ts +1 -0
- package/dist/editor/services/reviewsService.js +8 -0
- package/dist/editor/services/reviewsService.js.map +1 -1
- package/dist/editor/services/suggestedEditsService.d.ts +1 -0
- package/dist/editor/services/suggestedEditsService.js +8 -0
- package/dist/editor/services/suggestedEditsService.js.map +1 -1
- package/dist/revision.d.ts +2 -2
- package/dist/revision.js +2 -2
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
- package/src/config/config.tsx +7 -0
- package/src/editor/control-center/IndexOverview.tsx +67 -33
- package/src/editor/control-center/LatestFeedback.tsx +198 -0
- package/src/editor/field-types/richtext/contextMenuFactory.tsx +14 -0
- package/src/editor/menubar/toolbar-sections/EditControls.tsx +2 -3
- package/src/editor/menubar/toolbar-sections/ViewportControls.tsx +19 -1
- package/src/editor/reviews/Comment.tsx +16 -1
- package/src/editor/reviews/CommentView.tsx +24 -24
- package/src/editor/reviews/Comments.tsx +73 -181
- package/src/editor/reviews/SuggestedEdit.tsx +16 -10
- package/src/editor/reviews/SuggestionDisplayPopover.tsx +14 -9
- package/src/editor/services/reviewsService.ts +10 -0
- package/src/editor/services/suggestedEditsService.ts +10 -0
- package/src/revision.ts +2 -2
- package/src/types.ts +4 -0
package/package.json
CHANGED
package/src/config/config.tsx
CHANGED
|
@@ -79,6 +79,7 @@ import { Info } from "../editor/control-center/Info";
|
|
|
79
79
|
import { QuotaInfo } from "../editor/control-center/QuotaInfo";
|
|
80
80
|
import { About } from "../editor/control-center/About";
|
|
81
81
|
import { WebSocketMessages } from "../editor/control-center/WebSocketMessages";
|
|
82
|
+
import { LatestFeedback } from "../editor/control-center/LatestFeedback";
|
|
82
83
|
import { DbSetupStep } from "../editor/control-center/setup-steps/DbSetupStep";
|
|
83
84
|
import { SettingsSetupStep } from "../editor/control-center/setup-steps/SettingsSetupStep";
|
|
84
85
|
import { AiSetupStep } from "../editor/control-center/setup-steps/AiSetupStep";
|
|
@@ -579,6 +580,12 @@ export const getConfiguration = (): EditorConfiguration => {
|
|
|
579
580
|
initialSize: 100,
|
|
580
581
|
noOverflow: true,
|
|
581
582
|
},
|
|
583
|
+
{
|
|
584
|
+
name: "latest-feedback",
|
|
585
|
+
title: "Latest Feedback",
|
|
586
|
+
content: <LatestFeedback />,
|
|
587
|
+
initialSize: 50,
|
|
588
|
+
},
|
|
582
589
|
],
|
|
583
590
|
},
|
|
584
591
|
...pageEditorViewBase,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
RefreshCw,
|
|
4
|
+
ExternalLink,
|
|
4
5
|
Database,
|
|
5
6
|
Clock,
|
|
6
7
|
CheckCircle,
|
|
7
8
|
AlertCircle,
|
|
8
9
|
Trash2,
|
|
9
10
|
} from "lucide-react";
|
|
10
|
-
|
|
11
|
+
import { Button } from "../../components/ui/button";
|
|
11
12
|
import { classNames } from "primereact/utils";
|
|
12
13
|
import { formatDateOnly, formatDateTime } from "../utils";
|
|
13
14
|
import {
|
|
@@ -26,6 +27,8 @@ import {
|
|
|
26
27
|
startDirectGeneration,
|
|
27
28
|
} from "../services/indexService";
|
|
28
29
|
|
|
30
|
+
import { useEditContext } from "../client/editContext";
|
|
31
|
+
|
|
29
32
|
export function IndexOverview() {
|
|
30
33
|
const [indexStatus, setIndexStatus] = useState<IndexStatus | null>(null);
|
|
31
34
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -191,6 +194,54 @@ export function IndexOverview() {
|
|
|
191
194
|
}
|
|
192
195
|
};
|
|
193
196
|
|
|
197
|
+
const editContext = useEditContext();
|
|
198
|
+
const goToSettings = useCallback(() => {
|
|
199
|
+
// Ensure URL reflects the target view and panel before switching views
|
|
200
|
+
editContext?.updateUrl({ view: "control-center", ccpanel: "setup" });
|
|
201
|
+
editContext?.switchView("control-center");
|
|
202
|
+
}, [editContext]);
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
const getIndexStatusInfo = (
|
|
206
|
+
isCollecting: boolean,
|
|
207
|
+
isSubmitting: boolean,
|
|
208
|
+
indexStatus: IndexStatus | null,
|
|
209
|
+
importStatus: ImportStatus | null,
|
|
210
|
+
centroidsStatus: CentroidsStatus | null,
|
|
211
|
+
) => {
|
|
212
|
+
const batches = indexStatus?.batches || [];
|
|
213
|
+
const inProgress = batches.some((b) => {
|
|
214
|
+
const s = (b.status || "").toLowerCase();
|
|
215
|
+
return (
|
|
216
|
+
s === "submitted" || s === "processing" || s === "queued"
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
if (isCollecting) {
|
|
221
|
+
return { statusLabel: "Collecting items", statusClass: "text-indigo-600", hasSettingsIssue: false};
|
|
222
|
+
} else if (isSubmitting || inProgress) {
|
|
223
|
+
return { statusLabel: "Generating Embeddings", statusClass: "text-blue-600", hasSettingsIssue: false };
|
|
224
|
+
} else if (importStatus?.isImporting) {
|
|
225
|
+
return {
|
|
226
|
+
statusLabel: importStatus.status || "Importing embeddings",
|
|
227
|
+
statusClass: "text-purple-600",
|
|
228
|
+
hasSettingsIssue: false
|
|
229
|
+
};
|
|
230
|
+
} else if (centroidsStatus?.isPopulating) {
|
|
231
|
+
return {
|
|
232
|
+
statusLabel: centroidsStatus.status || "Populating centroids",
|
|
233
|
+
statusClass: "text-pink-600",
|
|
234
|
+
hasSettingsIssue: false
|
|
235
|
+
};
|
|
236
|
+
} else if (indexStatus?.rebuilding) {
|
|
237
|
+
return { statusLabel: "Rebuilding", statusClass: "text-orange-600", hasSettingsIssue: false };
|
|
238
|
+
} else if (indexStatus?.messages != null && indexStatus.messages.length > 0 && indexStatus.messages[0]?.message){
|
|
239
|
+
return { statusLabel: indexStatus.messages[0].message, statusClass: "text-red-600", hasSettingsIssue: true };
|
|
240
|
+
}else {
|
|
241
|
+
return { statusLabel: "Ready", statusClass: "text-green-600", hasSettingsIssue: false };
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
194
245
|
return (
|
|
195
246
|
<div className="flex h-full items-stretch">
|
|
196
247
|
<div className="flex min-w-96 flex-col gap-4 p-4">
|
|
@@ -376,41 +427,24 @@ export function IndexOverview() {
|
|
|
376
427
|
<div className="rounded bg-gray-50 p-3">
|
|
377
428
|
<div className="text-sm text-gray-600">Status</div>
|
|
378
429
|
{(() => {
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const statusLabel = isCollecting
|
|
387
|
-
? "Collecting items"
|
|
388
|
-
: isSubmitting || inProgress
|
|
389
|
-
? "Generating Embeddings"
|
|
390
|
-
: importStatus?.isImporting
|
|
391
|
-
? importStatus.status || "Importing embeddings"
|
|
392
|
-
: centroidsStatus?.isPopulating
|
|
393
|
-
? centroidsStatus.status || "Populating centroids"
|
|
394
|
-
: indexStatus.rebuilding
|
|
395
|
-
? "Rebuilding"
|
|
396
|
-
: "Ready";
|
|
397
|
-
const statusClass =
|
|
398
|
-
statusLabel === "Rebuilding"
|
|
399
|
-
? "text-orange-600"
|
|
400
|
-
: statusLabel === "Generating Embeddings"
|
|
401
|
-
? "text-blue-600"
|
|
402
|
-
: statusLabel === "Collecting items"
|
|
403
|
-
? "text-indigo-600"
|
|
404
|
-
: importStatus?.isImporting
|
|
405
|
-
? "text-purple-600"
|
|
406
|
-
: centroidsStatus?.isPopulating
|
|
407
|
-
? "text-pink-600"
|
|
408
|
-
: "text-green-600";
|
|
430
|
+
const { statusLabel, statusClass, hasSettingsIssue } = getIndexStatusInfo(
|
|
431
|
+
isCollecting,
|
|
432
|
+
isSubmitting,
|
|
433
|
+
indexStatus,
|
|
434
|
+
importStatus,
|
|
435
|
+
centroidsStatus,
|
|
436
|
+
);
|
|
409
437
|
return (
|
|
410
438
|
<div
|
|
411
|
-
className={classNames("text-sm font-medium", statusClass)}
|
|
439
|
+
className={classNames("text-sm font-medium flex items-center gap-2", statusClass)}
|
|
412
440
|
>
|
|
413
441
|
{statusLabel}
|
|
442
|
+
{hasSettingsIssue && (
|
|
443
|
+
<Button size="sm" variant="outline" onClick={goToSettings}>
|
|
444
|
+
<ExternalLink className="h-4 w-4" strokeWidth={1} />
|
|
445
|
+
Settings
|
|
446
|
+
</Button>
|
|
447
|
+
)}
|
|
414
448
|
</div>
|
|
415
449
|
);
|
|
416
450
|
})()}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useEditContext } from "../client/editContext";
|
|
3
|
+
import { Comment, SuggestedEdit } from "../../types";
|
|
4
|
+
import { getLatestComments } from "../services/reviewsService";
|
|
5
|
+
import { getLatestSuggestedEdits } from "../services/suggestedEditsService";
|
|
6
|
+
import { Lightbulb, MessageSquare } from "lucide-react";
|
|
7
|
+
|
|
8
|
+
type FeedbackItem =
|
|
9
|
+
| (Comment & { kind: "comment" })
|
|
10
|
+
| (SuggestedEdit & { kind: "suggestion" });
|
|
11
|
+
|
|
12
|
+
export function LatestFeedback() {
|
|
13
|
+
const editContext = useEditContext();
|
|
14
|
+
const [items, setItems] = useState<FeedbackItem[]>([]);
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
|
|
18
|
+
// Cutoff is now computed server-side. Keep memo in case we later want client hints.
|
|
19
|
+
const sinceIso = useMemo(() => null, []);
|
|
20
|
+
|
|
21
|
+
const load = useCallback(async () => {
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
setIsLoading(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
try {
|
|
26
|
+
const take = 50;
|
|
27
|
+
const [cRes, sRes] = await Promise.all([
|
|
28
|
+
getLatestComments(take),
|
|
29
|
+
getLatestSuggestedEdits(take),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const comments = ((cRes.data || []) as Comment[]).map((c) => ({
|
|
33
|
+
...(c as Comment),
|
|
34
|
+
kind: "comment" as const,
|
|
35
|
+
}));
|
|
36
|
+
const suggestions = ((sRes.data || []) as SuggestedEdit[]).map((s) => ({
|
|
37
|
+
...(s as SuggestedEdit),
|
|
38
|
+
kind: "suggestion" as const,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const merged = [...comments, ...suggestions].sort((a, b) => {
|
|
42
|
+
const ad = new Date((a as any).created || 0).getTime();
|
|
43
|
+
const bd = new Date((b as any).created || 0).getTime();
|
|
44
|
+
return bd - ad;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const latest = merged.slice(0, 50);
|
|
48
|
+
if (!cancelled) setItems(latest);
|
|
49
|
+
} catch (e: any) {
|
|
50
|
+
if (!cancelled) setError(e?.message || "Failed to load latest items");
|
|
51
|
+
} finally {
|
|
52
|
+
if (!cancelled) setIsLoading(false);
|
|
53
|
+
}
|
|
54
|
+
return () => {
|
|
55
|
+
cancelled = true;
|
|
56
|
+
};
|
|
57
|
+
}, [sinceIso]);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
let disposed: any;
|
|
61
|
+
(async () => {
|
|
62
|
+
disposed = await load();
|
|
63
|
+
})();
|
|
64
|
+
load();
|
|
65
|
+
return () => {
|
|
66
|
+
try {
|
|
67
|
+
disposed?.();
|
|
68
|
+
} catch {}
|
|
69
|
+
};
|
|
70
|
+
}, [load]);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const typesToRefresh = new Set([
|
|
74
|
+
"comment-updated",
|
|
75
|
+
"comment-deleted",
|
|
76
|
+
"suggested-edit-updated",
|
|
77
|
+
"suggested-edit-deleted",
|
|
78
|
+
]);
|
|
79
|
+
const dispose = editContext?.addSocketMessageListener?.((message: any) => {
|
|
80
|
+
try {
|
|
81
|
+
if (typesToRefresh.has(message?.type)) {
|
|
82
|
+
void load();
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
});
|
|
86
|
+
return () => {
|
|
87
|
+
try {
|
|
88
|
+
dispose?.();
|
|
89
|
+
} catch {}
|
|
90
|
+
};
|
|
91
|
+
}, [editContext?.addSocketMessageListener, load]);
|
|
92
|
+
|
|
93
|
+
const onOpenItem = async (item: FeedbackItem) => {
|
|
94
|
+
try {
|
|
95
|
+
const mainId =
|
|
96
|
+
item.kind === "comment" ? item.mainItemId : item.mainItemId;
|
|
97
|
+
const lang =
|
|
98
|
+
item.kind === "comment" ? item.language : item.mainItemLanguage;
|
|
99
|
+
const ver = item.kind === "comment" ? item.version : item.mainItemVersion;
|
|
100
|
+
await editContext?.loadItem({ id: mainId, language: lang, version: ver });
|
|
101
|
+
editContext?.setSelectedComment(
|
|
102
|
+
item.kind === "comment" ? (item as Comment) : undefined,
|
|
103
|
+
);
|
|
104
|
+
if (item.kind === "comment") {
|
|
105
|
+
editContext?.setScrollIntoView((item as Comment).itemId);
|
|
106
|
+
}
|
|
107
|
+
editContext?.switchView("comments");
|
|
108
|
+
} catch {}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const Row = ({ item }: { item: FeedbackItem }) => {
|
|
112
|
+
const isComment = item.kind === "comment";
|
|
113
|
+
const created = (item as any).created
|
|
114
|
+
? new Date((item as any).created)
|
|
115
|
+
: undefined;
|
|
116
|
+
const dateText = created
|
|
117
|
+
? `${created.toLocaleDateString()} ${created.toLocaleTimeString()}`
|
|
118
|
+
: "";
|
|
119
|
+
const comment = isComment ? (item as Comment) : undefined;
|
|
120
|
+
const pageName = isComment
|
|
121
|
+
? comment?.mainItemName ||
|
|
122
|
+
(comment?.relatedItems || []).find(
|
|
123
|
+
(ri) => ri.itemId === comment?.mainItemId,
|
|
124
|
+
)?.itemName
|
|
125
|
+
: (item as SuggestedEdit).mainItemName;
|
|
126
|
+
const componentName = isComment
|
|
127
|
+
? comment?.itemName
|
|
128
|
+
: (item as SuggestedEdit).itemName;
|
|
129
|
+
const fieldName = isComment ? comment?.fieldName : undefined;
|
|
130
|
+
const user = (item as any).authorDisplayName || (item as any).author || "";
|
|
131
|
+
const label = isComment ? "Comment" : "Suggestion";
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<button
|
|
135
|
+
className="w-full cursor-pointer rounded p-1 text-left hover:bg-gray-100"
|
|
136
|
+
onClick={() => onOpenItem(item)}
|
|
137
|
+
>
|
|
138
|
+
<div className="flex items-center justify-between gap-2 text-xs">
|
|
139
|
+
<div className="flex items-center gap-2 truncate">
|
|
140
|
+
<span
|
|
141
|
+
className="inline-flex items-center justify-center rounded py-0.5 text-gray-700"
|
|
142
|
+
aria-label={label}
|
|
143
|
+
title={label}
|
|
144
|
+
>
|
|
145
|
+
{isComment ? (
|
|
146
|
+
<MessageSquare className="h-3.5 w-3.5" strokeWidth={1} />
|
|
147
|
+
) : (
|
|
148
|
+
<Lightbulb className="h-3.5 w-3.5" strokeWidth={1} />
|
|
149
|
+
)}
|
|
150
|
+
</span>
|
|
151
|
+
<div className="flex items-center">
|
|
152
|
+
{[
|
|
153
|
+
pageName,
|
|
154
|
+
componentName,
|
|
155
|
+
fieldName ||
|
|
156
|
+
(item.kind === "suggestion" ? item.fieldId : undefined),
|
|
157
|
+
]
|
|
158
|
+
.filter((x): x is string => !!x && x.length > 0)
|
|
159
|
+
.map((seg, idx, arr) => (
|
|
160
|
+
<span key={idx} className="truncate">
|
|
161
|
+
{idx > 0 && (
|
|
162
|
+
<span className="mx-1 text-gray-400">></span>
|
|
163
|
+
)}
|
|
164
|
+
<span
|
|
165
|
+
className={
|
|
166
|
+
idx === 0
|
|
167
|
+
? "font-medium text-gray-900"
|
|
168
|
+
: "text-gray-500"
|
|
169
|
+
}
|
|
170
|
+
>
|
|
171
|
+
{seg}
|
|
172
|
+
</span>
|
|
173
|
+
</span>
|
|
174
|
+
))}
|
|
175
|
+
</div>
|
|
176
|
+
{user && <span className="text-gray-500"> · {user}</span>}
|
|
177
|
+
</div>
|
|
178
|
+
<div className="shrink-0 text-[10px] text-gray-500">{dateText}</div>
|
|
179
|
+
</div>
|
|
180
|
+
</button>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<div className="h-full overflow-auto p-2">
|
|
186
|
+
{isLoading && <div className="p-2 text-xs text-gray-500">Loading…</div>}
|
|
187
|
+
{error && <div className="p-2 text-xs text-red-600">{error}</div>}
|
|
188
|
+
{!isLoading && !error && items.length === 0 && (
|
|
189
|
+
<div className="p-2 text-xs text-gray-500">No recent activity</div>
|
|
190
|
+
)}
|
|
191
|
+
<div className="divide-y">
|
|
192
|
+
{items.map((it) => (
|
|
193
|
+
<Row key={`${it.kind}-${(it as any).id}`} item={it} />
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
@@ -3,6 +3,7 @@ import { Field } from "../../pageModel";
|
|
|
3
3
|
import { EditContextType } from "../../client/editContext";
|
|
4
4
|
import { getRichTextProfile } from "../../services/contentService";
|
|
5
5
|
import { getCachedParsedProfile } from "./utils/profileServiceCache";
|
|
6
|
+
import { getComponentById } from "../../componentTreeHelper";
|
|
6
7
|
import {
|
|
7
8
|
RichTextEditorProfile,
|
|
8
9
|
SLATE_MARKS,
|
|
@@ -82,6 +83,19 @@ export async function createRichTextContextMenu(
|
|
|
82
83
|
const richTextField = field as RichTextField;
|
|
83
84
|
const profilePath = richTextField.customProperties?.profile;
|
|
84
85
|
|
|
86
|
+
// Hide formatting when layout components are disabled and this field belongs to a layout component
|
|
87
|
+
try {
|
|
88
|
+
if (editContext?.showLayoutComponents === false && editContext?.page) {
|
|
89
|
+
const owner = getComponentById(
|
|
90
|
+
field.descriptor.item.id,
|
|
91
|
+
editContext.page,
|
|
92
|
+
);
|
|
93
|
+
if (owner?.layoutId) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} catch {}
|
|
98
|
+
|
|
85
99
|
// Get the iframe document
|
|
86
100
|
const iframeDocument =
|
|
87
101
|
editContext.pageView?.editorIframe?.contentWindow?.document;
|
|
@@ -18,12 +18,11 @@ export function EditControls({
|
|
|
18
18
|
if (!editContext) return null;
|
|
19
19
|
|
|
20
20
|
// Calculate total count of comments and suggestions
|
|
21
|
-
|
|
22
|
-
editContext.comments?.filter((x) => !x.isResolved).length || 0;
|
|
21
|
+
|
|
23
22
|
const suggestionsCount =
|
|
24
23
|
editContext.suggestedEdits?.filter((x) => x.status === "pending").length ||
|
|
25
24
|
0;
|
|
26
|
-
const totalCount =
|
|
25
|
+
const totalCount = suggestionsCount;
|
|
27
26
|
|
|
28
27
|
return (
|
|
29
28
|
<>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CompareIcon } from "../../ui/Icons";
|
|
2
2
|
import { useEditContext } from "../../client/editContext";
|
|
3
3
|
import { SimpleIconButton } from "../../ui/SimpleIconButton";
|
|
4
|
-
import { Monitor, Smartphone, Table } from "lucide-react";
|
|
4
|
+
import { Monitor, Smartphone, Table, MessageSquareMore } from "lucide-react";
|
|
5
5
|
import { Separator } from "../Separator";
|
|
6
6
|
|
|
7
7
|
export function ViewportControls() {
|
|
@@ -12,6 +12,8 @@ export function ViewportControls() {
|
|
|
12
12
|
const pageViewContext = editContext.pageView;
|
|
13
13
|
const device = pageViewContext.device;
|
|
14
14
|
const setDevice = pageViewContext.setDevice;
|
|
15
|
+
const commentsCount =
|
|
16
|
+
editContext.comments?.filter((x) => !x.isResolved).length || 0;
|
|
15
17
|
|
|
16
18
|
return (
|
|
17
19
|
<>
|
|
@@ -56,6 +58,22 @@ export function ViewportControls() {
|
|
|
56
58
|
onClick={() => editContext.setCompareMode(!editContext.compareMode)}
|
|
57
59
|
/>
|
|
58
60
|
)}
|
|
61
|
+
<Separator size="large" />{" "}
|
|
62
|
+
<SimpleIconButton
|
|
63
|
+
className="relative"
|
|
64
|
+
selected={!!editContext.showComments}
|
|
65
|
+
icon={<MessageSquareMore className="h-6 w-6 p-1" strokeWidth={1} />}
|
|
66
|
+
label={editContext.showComments ? "Hide comments" : "Show comments"}
|
|
67
|
+
size="large"
|
|
68
|
+
data-testid="toggle-comments-button"
|
|
69
|
+
onClick={() => editContext.setShowComments(!editContext.showComments)}
|
|
70
|
+
>
|
|
71
|
+
{commentsCount > 0 && (
|
|
72
|
+
<div className="bg-theme-secondary text-3xs absolute -top-1 -right-1 flex h-[16px] min-w-[16px] items-center justify-center rounded-full px-1 text-white">
|
|
73
|
+
{commentsCount > 99 ? "99+" : commentsCount}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</SimpleIconButton>
|
|
59
77
|
</>
|
|
60
78
|
);
|
|
61
79
|
}
|
|
@@ -136,7 +136,20 @@ export function Comment({
|
|
|
136
136
|
className={`mb-3 cursor-pointer rounded-lg border-2 bg-white p-3 shadow-sm hover:bg-gray-50 ${
|
|
137
137
|
isSelected ? "border-blue-500" : "border-transparent"
|
|
138
138
|
}`}
|
|
139
|
-
onClick={() => {
|
|
139
|
+
onClick={async () => {
|
|
140
|
+
const currentPageId = editContext?.currentItemDescriptor?.id;
|
|
141
|
+
if (
|
|
142
|
+
currentPageId &&
|
|
143
|
+
comment.mainItemId &&
|
|
144
|
+
comment.mainItemId !== currentPageId
|
|
145
|
+
) {
|
|
146
|
+
await editContext?.loadItem({
|
|
147
|
+
id: comment.mainItemId,
|
|
148
|
+
language: comment.language,
|
|
149
|
+
version: comment.version,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
140
153
|
editContext?.setSelectedComment(comment);
|
|
141
154
|
editContext?.setScrollIntoView(comment.itemId);
|
|
142
155
|
editContext?.select([comment.itemId]);
|
|
@@ -161,6 +174,8 @@ export function Comment({
|
|
|
161
174
|
canDelete={canDelete}
|
|
162
175
|
canResolve={canResolve}
|
|
163
176
|
availableTags={availableTags}
|
|
177
|
+
basePageName={editContext?.currentItemDescriptor?.name}
|
|
178
|
+
basePageId={editContext?.currentItemDescriptor?.id}
|
|
164
179
|
onEdit={() => setIsEditing(true)}
|
|
165
180
|
onDelete={handleDelete}
|
|
166
181
|
onResolve={handleResolve}
|
|
@@ -16,6 +16,11 @@ export interface CommentViewProps {
|
|
|
16
16
|
canDelete: boolean;
|
|
17
17
|
canResolve: boolean;
|
|
18
18
|
availableTags?: { label: string; color: string }[];
|
|
19
|
+
// The id of the page currently open in the editor. Used to decide when to
|
|
20
|
+
// show the page name for comments belonging to a different page (child page).
|
|
21
|
+
basePageId?: string;
|
|
22
|
+
// The name of the page to display in the breadcrumb
|
|
23
|
+
basePageName?: string;
|
|
19
24
|
onEdit?: () => void;
|
|
20
25
|
onDelete?: () => void;
|
|
21
26
|
onResolve?: () => void;
|
|
@@ -31,6 +36,8 @@ export function CommentView({
|
|
|
31
36
|
canDelete,
|
|
32
37
|
canResolve,
|
|
33
38
|
availableTags = [],
|
|
39
|
+
basePageId,
|
|
40
|
+
basePageName,
|
|
34
41
|
onEdit,
|
|
35
42
|
onDelete,
|
|
36
43
|
onResolve,
|
|
@@ -73,36 +80,29 @@ export function CommentView({
|
|
|
73
80
|
};
|
|
74
81
|
|
|
75
82
|
const renderContextInfo = () => {
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
const pageName = basePageName;
|
|
84
|
+
const isComponentDifferent =
|
|
85
|
+
!!comment.itemId && comment.mainItemId !== comment.itemId;
|
|
86
|
+
const componentName = isComponentDifferent ? comment.itemName : undefined;
|
|
87
|
+
const fieldName = comment.fieldName;
|
|
79
88
|
|
|
80
|
-
|
|
89
|
+
const segments = [pageName, componentName, fieldName].filter(
|
|
90
|
+
(x): x is string => !!x && x.length > 0,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (segments.length === 0) return null;
|
|
81
94
|
|
|
82
95
|
return (
|
|
83
96
|
<div
|
|
84
97
|
className={`${compact ? "mt-2" : "mt-3"} flex items-center border-t pt-${compact ? "2" : "3"} text-xs`}
|
|
85
98
|
data-testid="comment-context-info"
|
|
86
99
|
>
|
|
87
|
-
{
|
|
88
|
-
<
|
|
89
|
-
className="text-2xs text-gray-500"
|
|
90
|
-
|
|
91
|
-
>
|
|
92
|
-
|
|
93
|
-
</div>
|
|
94
|
-
)}
|
|
95
|
-
{showFieldName && showItemName && (
|
|
96
|
-
<div className="text-2xs mx-2 text-gray-500">></div>
|
|
97
|
-
)}
|
|
98
|
-
{showFieldName && (
|
|
99
|
-
<div
|
|
100
|
-
className="text-2xs text-gray-500"
|
|
101
|
-
data-testid="comment-field-name"
|
|
102
|
-
>
|
|
103
|
-
{comment.fieldName}
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
100
|
+
{segments.map((seg, idx) => (
|
|
101
|
+
<React.Fragment key={idx}>
|
|
102
|
+
{idx > 0 && <div className="text-2xs mx-2 text-gray-500">></div>}
|
|
103
|
+
<div className="text-2xs text-gray-500">{seg}</div>
|
|
104
|
+
</React.Fragment>
|
|
105
|
+
))}
|
|
106
106
|
</div>
|
|
107
107
|
);
|
|
108
108
|
};
|
|
@@ -115,7 +115,7 @@ export function CommentView({
|
|
|
115
115
|
>
|
|
116
116
|
<div>
|
|
117
117
|
<div
|
|
118
|
-
className={
|
|
118
|
+
className={`text-dark text-xs font-medium`}
|
|
119
119
|
title={comment.author}
|
|
120
120
|
>
|
|
121
121
|
{comment.authorDisplayName}
|