@alpaca-editor/core 1.0.4104 → 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/MainLayout.js +1 -1
- package/dist/editor/MainLayout.js.map +1 -1
- package/dist/editor/Terminal.js +2 -2
- package/dist/editor/Terminal.js.map +1 -1
- package/dist/editor/ai/AgentTerminal.js +5 -3
- package/dist/editor/ai/AgentTerminal.js.map +1 -1
- package/dist/editor/ai/Agents.js +9 -1
- package/dist/editor/ai/Agents.js.map +1 -1
- package/dist/editor/ai/AiResponseMessage.js +4 -2
- package/dist/editor/ai/AiResponseMessage.js.map +1 -1
- package/dist/editor/client/EditorShell.js +2 -0
- package/dist/editor/client/EditorShell.js.map +1 -1
- package/dist/editor/control-center/IndexOverview.js +51 -32
- 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/control-center/WebSocketMessages.js +3 -3
- package/dist/editor/control-center/WebSocketMessages.js.map +1 -1
- package/dist/editor/field-types/richtext/contextMenuFactory.js +11 -0
- package/dist/editor/field-types/richtext/contextMenuFactory.js.map +1 -1
- package/dist/editor/media-selector/MediaFolderBrowser.js +2 -2
- package/dist/editor/media-selector/MediaFolderBrowser.js.map +1 -1
- package/dist/editor/menubar/ActiveUsers.js +3 -2
- package/dist/editor/menubar/ActiveUsers.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 +64 -76
- 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/editor/sidebar/Completions.js +2 -1
- package/dist/editor/sidebar/Completions.js.map +1 -1
- package/dist/editor/ui/Icons.d.ts +2 -1
- package/dist/editor/ui/Icons.js +2 -2
- package/dist/editor/ui/Icons.js.map +1 -1
- package/dist/editor/ui/SimpleTable.js +1 -1
- package/dist/editor/ui/SimpleTable.js.map +1 -1
- package/dist/editor/utils.d.ts +12 -1
- package/dist/editor/utils.js +60 -12
- package/dist/editor/utils.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/MainLayout.tsx +1 -1
- package/src/editor/Terminal.tsx +2 -2
- package/src/editor/ai/AgentTerminal.tsx +8 -20
- package/src/editor/ai/Agents.tsx +14 -2
- package/src/editor/ai/AiResponseMessage.tsx +7 -5
- package/src/editor/client/EditorShell.tsx +2 -0
- package/src/editor/control-center/IndexOverview.tsx +70 -35
- package/src/editor/control-center/LatestFeedback.tsx +198 -0
- package/src/editor/control-center/WebSocketMessages.tsx +3 -5
- package/src/editor/field-types/richtext/contextMenuFactory.tsx +14 -0
- package/src/editor/media-selector/MediaFolderBrowser.tsx +2 -2
- package/src/editor/menubar/ActiveUsers.tsx +4 -3
- 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 +122 -145
- 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/editor/sidebar/Completions.tsx +2 -1
- package/src/editor/ui/Icons.tsx +3 -0
- package/src/editor/ui/SimpleTable.tsx +2 -2
- package/src/editor/utils.ts +73 -15
- package/src/revision.ts +2 -2
- package/src/types.ts +4 -0
|
@@ -1,14 +1,16 @@
|
|
|
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";
|
|
13
|
+
import { formatDateOnly, formatDateTime } from "../utils";
|
|
12
14
|
import {
|
|
13
15
|
IndexStatus,
|
|
14
16
|
StagingStatus,
|
|
@@ -25,6 +27,8 @@ import {
|
|
|
25
27
|
startDirectGeneration,
|
|
26
28
|
} from "../services/indexService";
|
|
27
29
|
|
|
30
|
+
import { useEditContext } from "../client/editContext";
|
|
31
|
+
|
|
28
32
|
export function IndexOverview() {
|
|
29
33
|
const [indexStatus, setIndexStatus] = useState<IndexStatus | null>(null);
|
|
30
34
|
const [isLoading, setIsLoading] = useState(false);
|
|
@@ -190,6 +194,54 @@ export function IndexOverview() {
|
|
|
190
194
|
}
|
|
191
195
|
};
|
|
192
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
|
+
|
|
193
245
|
return (
|
|
194
246
|
<div className="flex h-full items-stretch">
|
|
195
247
|
<div className="flex min-w-96 flex-col gap-4 p-4">
|
|
@@ -375,41 +427,24 @@ export function IndexOverview() {
|
|
|
375
427
|
<div className="rounded bg-gray-50 p-3">
|
|
376
428
|
<div className="text-sm text-gray-600">Status</div>
|
|
377
429
|
{(() => {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const statusLabel = isCollecting
|
|
386
|
-
? "Collecting items"
|
|
387
|
-
: isSubmitting || inProgress
|
|
388
|
-
? "Generating Embeddings"
|
|
389
|
-
: importStatus?.isImporting
|
|
390
|
-
? importStatus.status || "Importing embeddings"
|
|
391
|
-
: centroidsStatus?.isPopulating
|
|
392
|
-
? centroidsStatus.status || "Populating centroids"
|
|
393
|
-
: indexStatus.rebuilding
|
|
394
|
-
? "Rebuilding"
|
|
395
|
-
: "Ready";
|
|
396
|
-
const statusClass =
|
|
397
|
-
statusLabel === "Rebuilding"
|
|
398
|
-
? "text-orange-600"
|
|
399
|
-
: statusLabel === "Generating Embeddings"
|
|
400
|
-
? "text-blue-600"
|
|
401
|
-
: statusLabel === "Collecting items"
|
|
402
|
-
? "text-indigo-600"
|
|
403
|
-
: importStatus?.isImporting
|
|
404
|
-
? "text-purple-600"
|
|
405
|
-
: centroidsStatus?.isPopulating
|
|
406
|
-
? "text-pink-600"
|
|
407
|
-
: "text-green-600";
|
|
430
|
+
const { statusLabel, statusClass, hasSettingsIssue } = getIndexStatusInfo(
|
|
431
|
+
isCollecting,
|
|
432
|
+
isSubmitting,
|
|
433
|
+
indexStatus,
|
|
434
|
+
importStatus,
|
|
435
|
+
centroidsStatus,
|
|
436
|
+
);
|
|
408
437
|
return (
|
|
409
438
|
<div
|
|
410
|
-
className={classNames("text-sm font-medium", statusClass)}
|
|
439
|
+
className={classNames("text-sm font-medium flex items-center gap-2", statusClass)}
|
|
411
440
|
>
|
|
412
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
|
+
)}
|
|
413
448
|
</div>
|
|
414
449
|
);
|
|
415
450
|
})()}
|
|
@@ -527,7 +562,7 @@ export function IndexOverview() {
|
|
|
527
562
|
</div>
|
|
528
563
|
<div className="text-xs text-gray-500">
|
|
529
564
|
{batch.created
|
|
530
|
-
? new Date(batch.created)
|
|
565
|
+
? formatDateOnly(new Date(batch.created))
|
|
531
566
|
: "Unknown"}
|
|
532
567
|
</div>
|
|
533
568
|
</div>
|
|
@@ -584,7 +619,7 @@ export function IndexOverview() {
|
|
|
584
619
|
)}
|
|
585
620
|
</div>
|
|
586
621
|
<div className="ml-2 shrink-0 text-xs text-gray-500">
|
|
587
|
-
{new Date(it.created)
|
|
622
|
+
{formatDateTime(new Date(it.created))}
|
|
588
623
|
</div>
|
|
589
624
|
</div>
|
|
590
625
|
))}
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { formatTime } from "../utils";
|
|
2
3
|
import { useEditContext } from "../client/editContext";
|
|
3
4
|
|
|
4
5
|
export function WebSocketMessages() {
|
|
@@ -34,11 +35,8 @@ export function WebSocketMessages() {
|
|
|
34
35
|
|
|
35
36
|
const formatTimestamp = (isoString: string) => {
|
|
36
37
|
const date = new Date(isoString);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"." +
|
|
40
|
-
date.getMilliseconds().toString().padStart(3, "0")
|
|
41
|
-
);
|
|
38
|
+
const base = formatTime(date);
|
|
39
|
+
return base + "." + date.getMilliseconds().toString().padStart(3, "0");
|
|
42
40
|
};
|
|
43
41
|
|
|
44
42
|
const getMessageTypeColor = (type: string) => {
|
|
@@ -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;
|
|
@@ -195,7 +195,7 @@ export function MediaFolderBrowser({
|
|
|
195
195
|
/>
|
|
196
196
|
</div>
|
|
197
197
|
<div className="min-w-0 flex-1">
|
|
198
|
-
<div className="truncate text-
|
|
198
|
+
<div className="text-dark truncate text-xs font-medium">
|
|
199
199
|
{t.name}
|
|
200
200
|
</div>
|
|
201
201
|
<div className="mt-1 text-xs text-gray-500">Media File</div>
|
|
@@ -305,7 +305,7 @@ export function MediaFolderBrowser({
|
|
|
305
305
|
{/* Selected Item Info */}
|
|
306
306
|
{selectedImage && (
|
|
307
307
|
<div className="border-t bg-gray-50 p-3">
|
|
308
|
-
<div className="truncate text-
|
|
308
|
+
<div className="text-dark truncate text-xs font-medium">
|
|
309
309
|
Selected: {selectedImage.name}
|
|
310
310
|
</div>
|
|
311
311
|
<div className="mt-1 text-xs text-gray-500">
|
|
@@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
|
|
|
9
9
|
import { getItemVisitors, ItemVisitor } from "../services/editService";
|
|
10
10
|
import { VerticalDotsIcon } from "../ui/Icons";
|
|
11
11
|
import { AboutDialog } from "../client/AboutDialog";
|
|
12
|
+
import { formatDateOnly } from "../utils";
|
|
12
13
|
|
|
13
14
|
type UserListItem = {
|
|
14
15
|
type: "active" | "visitor";
|
|
@@ -258,7 +259,7 @@ export function ActiveUsers() {
|
|
|
258
259
|
/>
|
|
259
260
|
</div>
|
|
260
261
|
<div className="min-w-0 flex-1">
|
|
261
|
-
<div className="text-
|
|
262
|
+
<div className="text-dark text-xs font-medium">About</div>
|
|
262
263
|
</div>
|
|
263
264
|
</div>
|
|
264
265
|
<div
|
|
@@ -272,7 +273,7 @@ export function ActiveUsers() {
|
|
|
272
273
|
/>
|
|
273
274
|
</div>
|
|
274
275
|
<div className="min-w-0 flex-1">
|
|
275
|
-
<div className="text-
|
|
276
|
+
<div className="text-dark text-xs font-medium">Log Out</div>
|
|
276
277
|
</div>
|
|
277
278
|
</div>
|
|
278
279
|
</div>
|
|
@@ -296,5 +297,5 @@ function formatTimeAgo(date: Date): string {
|
|
|
296
297
|
if (diffInSeconds < 604800)
|
|
297
298
|
return `${Math.floor(diffInSeconds / 86400)}d ago`;
|
|
298
299
|
|
|
299
|
-
return date
|
|
300
|
+
return formatDateOnly(date);
|
|
300
301
|
}
|
|
@@ -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}
|