@parhelia/localization 0.1.12911 → 0.1.12917
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/LocalizeItemDialog.d.ts.map +1 -1
- package/dist/LocalizeItemDialog.js +17 -9
- package/dist/LocalizeItemUtils.d.ts.map +1 -1
- package/dist/LocalizeItemUtils.js +8 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -9
- package/dist/steps/ItemSelectionStep.js +1 -1
- package/dist/steps/ItemSelectionTree.d.ts.map +1 -1
- package/dist/steps/ItemSelectionTree.js +6 -1
- package/dist/steps/ProviderStep.d.ts +3 -0
- package/dist/steps/ProviderStep.d.ts.map +1 -0
- package/dist/steps/ProviderStep.js +223 -0
- package/dist/steps/ServiceLanguageSelectionStep.d.ts.map +1 -1
- package/dist/steps/ServiceLanguageSelectionStep.js +11 -6
- package/dist/steps/WizardStepShell.d.ts +3 -1
- package/dist/steps/WizardStepShell.d.ts.map +1 -1
- package/dist/steps/WizardStepShell.js +4 -2
- package/dist/translation-center/TranslationBatches.d.ts +2 -0
- package/dist/translation-center/TranslationBatches.d.ts.map +1 -1
- package/dist/translation-center/TranslationBatches.js +568 -255
- package/dist/translation-center/TranslationsTitlebar.d.ts.map +1 -1
- package/dist/translation-center/TranslationsTitlebar.js +4 -13
- package/package.json +1 -1
- package/dist/steps/PromptCustomizationStep.d.ts +0 -3
- package/dist/steps/PromptCustomizationStep.d.ts.map +0 -1
- package/dist/steps/PromptCustomizationStep.js +0 -257
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState, useMemo, useSyncExternalStore, } from "react";
|
|
3
3
|
import React from "react";
|
|
4
|
-
import { Button, Badge, Select, Input, UserPicker, Popover, PopoverContent, PopoverTrigger, ContentTree, fetchItemStubs, searchUsers as searchBackendUsers, useEditContext, getLanguages, SimpleIconButton,
|
|
5
|
-
import { listBatches, getTranslationProviders, listBatchTranslationJobs, retryBatchTranslation,
|
|
4
|
+
import { Button, Badge, Select, Input, UserPicker, Popover, PopoverContent, PopoverTrigger, Tooltip, TooltipContent, TooltipTrigger, ContentTree, PageHeader, fetchItemStubs, searchUsers as searchBackendUsers, useEditContext, getLanguages, SimpleIconButton, formatDateTime, } from "@parhelia/core";
|
|
5
|
+
import { listBatches, getTranslationProviders, listBatchTranslationJobs, retryBatchTranslation, abortJob, abortBatch, } from "../services/translationService";
|
|
6
6
|
import { TRANSLATION_BATCH_STARTED_EVENT, } from "../translationEvents";
|
|
7
7
|
import { useDebouncedCallback } from "use-debounce";
|
|
8
|
-
import { X, ChevronDown, Languages, Loader2,
|
|
8
|
+
import { X, ChevronDown, Languages, Loader2, Globe, FolderTree, ExternalLink, Check, AlertCircle, CircleStop, Hourglass, Copy, Sparkles, RotateCcw, Plus, } from "lucide-react";
|
|
9
9
|
import { toast } from "sonner";
|
|
10
|
+
import { openLocalizeItemDialog } from "../LocalizeItemCommand";
|
|
10
11
|
// Wrappers for lucide-react icons to work with React 19
|
|
11
12
|
const XIcon = (props) => React.createElement(X, props);
|
|
12
13
|
const ChevronDownIcon = (props) => React.createElement(ChevronDown, props);
|
|
13
14
|
const LanguagesIcon = (props) => React.createElement(Languages, props);
|
|
14
15
|
const LoaderIcon = (props) => React.createElement(Loader2, props);
|
|
15
|
-
const CalendarIcon = (props) => React.createElement(Calendar, props);
|
|
16
|
-
const StatusIcon = (props) => React.createElement(CheckCircle2, props);
|
|
17
|
-
const UserFilterIcon = (props) => React.createElement(UserIcon2, props);
|
|
18
|
-
const ProviderIcon = (props) => React.createElement(Cloud, props);
|
|
19
16
|
const GlobeIcon = (props) => React.createElement(Globe, props);
|
|
20
|
-
const ItemIcon = (props) => React.createElement(FileText, props);
|
|
21
17
|
const FolderTreeIcon = (props) => React.createElement(FolderTree, props);
|
|
22
18
|
const ExternalLinkIcon = (props) => React.createElement(ExternalLink, props);
|
|
23
19
|
const CheckIcon = (props) => React.createElement(Check, props);
|
|
@@ -27,6 +23,70 @@ const QueuedIcon = (props) => React.createElement(Hourglass, props);
|
|
|
27
23
|
const CopyIcon = (props) => React.createElement(Copy, props);
|
|
28
24
|
const SparklesIcon = (props) => React.createElement(Sparkles, props);
|
|
29
25
|
const RetryIcon = (props) => React.createElement(RotateCcw, props);
|
|
26
|
+
const PlusIcon = (props) => React.createElement(Plus, props);
|
|
27
|
+
function renderTargetLanguageIcon(language) {
|
|
28
|
+
if (language.icon) {
|
|
29
|
+
return React.createElement("img", {
|
|
30
|
+
src: language.icon,
|
|
31
|
+
className: "h-4 w-5 object-cover",
|
|
32
|
+
alt: "",
|
|
33
|
+
"aria-hidden": true,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return React.createElement(GlobeIcon, {
|
|
37
|
+
className: "h-3.5 w-3.5 text-neutral-grey-50",
|
|
38
|
+
strokeWidth: 1.5,
|
|
39
|
+
"aria-hidden": true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const DEFAULT_FILTERS = {
|
|
43
|
+
dateRange: "last30days",
|
|
44
|
+
status: "all",
|
|
45
|
+
user: "all",
|
|
46
|
+
provider: "all",
|
|
47
|
+
targetLanguage: "all",
|
|
48
|
+
itemIdOrPath: "",
|
|
49
|
+
itemIncludeSubitems: false,
|
|
50
|
+
};
|
|
51
|
+
let filterSnapshot = {
|
|
52
|
+
filters: DEFAULT_FILTERS,
|
|
53
|
+
itemIdOrPathInput: "",
|
|
54
|
+
};
|
|
55
|
+
let defaultUserInitialized = false;
|
|
56
|
+
const filterListeners = new Set();
|
|
57
|
+
function emitFilterSnapshot() {
|
|
58
|
+
filterListeners.forEach((listener) => listener());
|
|
59
|
+
}
|
|
60
|
+
function subscribeFilterSnapshot(listener) {
|
|
61
|
+
filterListeners.add(listener);
|
|
62
|
+
return () => filterListeners.delete(listener);
|
|
63
|
+
}
|
|
64
|
+
function getFilterSnapshot() {
|
|
65
|
+
return filterSnapshot;
|
|
66
|
+
}
|
|
67
|
+
function setTranslationBatchFilters(updater) {
|
|
68
|
+
const nextFilters = typeof updater === "function" ? updater(filterSnapshot.filters) : updater;
|
|
69
|
+
filterSnapshot = { ...filterSnapshot, filters: nextFilters };
|
|
70
|
+
emitFilterSnapshot();
|
|
71
|
+
}
|
|
72
|
+
function setTranslationBatchItemInput(value) {
|
|
73
|
+
filterSnapshot = { ...filterSnapshot, itemIdOrPathInput: value };
|
|
74
|
+
emitFilterSnapshot();
|
|
75
|
+
}
|
|
76
|
+
function ensureDefaultTranslationBatchUser(currentUserName) {
|
|
77
|
+
if (defaultUserInitialized || currentUserName === "all")
|
|
78
|
+
return;
|
|
79
|
+
defaultUserInitialized = true;
|
|
80
|
+
if (filterSnapshot.filters.user !== "all")
|
|
81
|
+
return;
|
|
82
|
+
setTranslationBatchFilters((previous) => ({
|
|
83
|
+
...previous,
|
|
84
|
+
user: currentUserName,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
function useTranslationBatchFilterSnapshot() {
|
|
88
|
+
return useSyncExternalStore(subscribeFilterSnapshot, getFilterSnapshot, getFilterSnapshot);
|
|
89
|
+
}
|
|
30
90
|
const DATE_RANGE_OPTIONS = [
|
|
31
91
|
{ value: "lastHour", label: "Last Hour" },
|
|
32
92
|
{ value: "last24hours", label: "Last 24 Hours" },
|
|
@@ -78,6 +138,9 @@ function normalizeGuid(value) {
|
|
|
78
138
|
.replace(/[{}()]/g, "")
|
|
79
139
|
.toLowerCase();
|
|
80
140
|
}
|
|
141
|
+
function normalizeLanguageCode(value) {
|
|
142
|
+
return (value ?? "").toString().trim().toLowerCase();
|
|
143
|
+
}
|
|
81
144
|
function dateRangeToFromUtc(range) {
|
|
82
145
|
const now = new Date();
|
|
83
146
|
const cutoff = new Date(now);
|
|
@@ -114,20 +177,17 @@ function dateRangeToFromUtc(range) {
|
|
|
114
177
|
export function TranslationBatches() {
|
|
115
178
|
const editContext = useEditContext();
|
|
116
179
|
const [batches, setBatches] = useState([]);
|
|
117
|
-
const [providers, setProviders] = useState([]);
|
|
118
180
|
const [sitecoreLanguages, setSitecoreLanguages] = useState([]);
|
|
119
181
|
const [isLoading, setIsLoading] = useState(false);
|
|
120
182
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
121
183
|
const [hasMore, setHasMore] = useState(true);
|
|
122
184
|
const [currentOffset, setCurrentOffset] = useState(0);
|
|
123
|
-
const [itemIdOrPathInput, setItemIdOrPathInput] = useState("");
|
|
124
|
-
const [itemPickerOpen, setItemPickerOpen] = useState(false);
|
|
125
185
|
const [expandedBatchId, setExpandedBatchId] = useState(null);
|
|
126
186
|
const [expandedItems, setExpandedItems] = useState(new Set());
|
|
127
187
|
const [batchJobs, setBatchJobs] = useState({});
|
|
128
188
|
const [loadingJobs, setLoadingJobs] = useState(new Set());
|
|
129
189
|
const [abortingBatchIds, setAbortingBatchIds] = useState(new Set());
|
|
130
|
-
const [
|
|
190
|
+
const [abortingJobIds, setAbortingJobIds] = useState(new Set());
|
|
131
191
|
const [retryingKeys, setRetryingKeys] = useState(new Set());
|
|
132
192
|
// Cache of userName → displayName so we can render full names on batch rows
|
|
133
193
|
// without re-querying the user service every render.
|
|
@@ -138,21 +198,41 @@ export function TranslationBatches() {
|
|
|
138
198
|
const requestIdRef = useRef(0);
|
|
139
199
|
const currentUserName = editContext?.user?.name || "all";
|
|
140
200
|
const isLimitedPreviewUser = editContext?.user?.isLimitedPreviewUser ?? false;
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
201
|
+
const isMobile = editContext?.isMobile ?? false;
|
|
202
|
+
const rootRef = useRef(null);
|
|
203
|
+
const [isCompactBatches, setIsCompactBatches] = useState(isMobile);
|
|
204
|
+
const { filters } = useTranslationBatchFilterSnapshot();
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const node = rootRef.current;
|
|
207
|
+
if (!node)
|
|
208
|
+
return;
|
|
209
|
+
const updateLayout = () => {
|
|
210
|
+
setIsCompactBatches(isMobile || node.clientWidth < 640);
|
|
211
|
+
};
|
|
212
|
+
updateLayout();
|
|
213
|
+
let observer;
|
|
214
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
215
|
+
observer = new ResizeObserver(updateLayout);
|
|
216
|
+
observer.observe(node);
|
|
217
|
+
}
|
|
218
|
+
window.addEventListener("resize", updateLayout);
|
|
219
|
+
return () => {
|
|
220
|
+
observer?.disconnect();
|
|
221
|
+
window.removeEventListener("resize", updateLayout);
|
|
222
|
+
};
|
|
223
|
+
}, [isMobile]);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
ensureDefaultTranslationBatchUser(currentUserName);
|
|
226
|
+
}, [currentUserName]);
|
|
150
227
|
// Enforce current user filter for limited preview users
|
|
151
228
|
useEffect(() => {
|
|
152
229
|
if (isLimitedPreviewUser &&
|
|
153
230
|
currentUserName !== "all" &&
|
|
154
231
|
filters.user !== currentUserName) {
|
|
155
|
-
|
|
232
|
+
setTranslationBatchFilters((prev) => ({
|
|
233
|
+
...prev,
|
|
234
|
+
user: currentUserName,
|
|
235
|
+
}));
|
|
156
236
|
}
|
|
157
237
|
}, [isLimitedPreviewUser, currentUserName, filters.user]);
|
|
158
238
|
const effectiveFilters = useMemo(() => {
|
|
@@ -220,29 +300,12 @@ export function TranslationBatches() {
|
|
|
220
300
|
}
|
|
221
301
|
}, []);
|
|
222
302
|
const debouncedReload = useDebouncedCallback(() => loadRecentBatches(0, false, effectiveFilters), 800);
|
|
223
|
-
const debouncedSetItemFilter = useDebouncedCallback((value) => setFilters((prev) => ({ ...prev, itemIdOrPath: value })), 400);
|
|
224
303
|
const loadMore = () => {
|
|
225
304
|
if (!isLoadingMore && hasMore) {
|
|
226
305
|
loadRecentBatches(currentOffset, true, effectiveFilters);
|
|
227
306
|
}
|
|
228
307
|
};
|
|
229
|
-
// Load
|
|
230
|
-
useEffect(() => {
|
|
231
|
-
const loadProviders = async () => {
|
|
232
|
-
try {
|
|
233
|
-
const res = await getTranslationProviders();
|
|
234
|
-
const maybeProviders = res?.data ?? res;
|
|
235
|
-
setProviders(Array.isArray(maybeProviders)
|
|
236
|
-
? maybeProviders
|
|
237
|
-
: []);
|
|
238
|
-
}
|
|
239
|
-
catch (error) {
|
|
240
|
-
console.error("Failed to load translation providers:", error);
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
loadProviders();
|
|
244
|
-
}, []);
|
|
245
|
-
// Load Sitecore languages once for the Target Language dropdown
|
|
308
|
+
// Load Sitecore languages for language chips in expanded batches.
|
|
246
309
|
useEffect(() => {
|
|
247
310
|
const loadLanguages = async () => {
|
|
248
311
|
try {
|
|
@@ -385,13 +448,6 @@ export function TranslationBatches() {
|
|
|
385
448
|
// refresh that brings the UI to its final state.
|
|
386
449
|
};
|
|
387
450
|
}, [editContext]);
|
|
388
|
-
const searchTranslationUsers = useCallback((query, limit) => searchBackendUsers(query, limit), []);
|
|
389
|
-
// Languages available in the Target Language dropdown — sourced from Sitecore so
|
|
390
|
-
// the list is stable regardless of the current filter selection.
|
|
391
|
-
const languageOptions = useMemo(() => sitecoreLanguages
|
|
392
|
-
.map((l) => l.languageCode || l.name)
|
|
393
|
-
.filter((code) => !!code)
|
|
394
|
-
.sort(), [sitecoreLanguages]);
|
|
395
451
|
// Group by date. Filtering happens server-side, so we operate on `batches` directly.
|
|
396
452
|
const groupedBatches = useMemo(() => {
|
|
397
453
|
if (batches.length === 0)
|
|
@@ -547,41 +603,54 @@ export function TranslationBatches() {
|
|
|
547
603
|
loadBatchJobs,
|
|
548
604
|
loadRecentBatches,
|
|
549
605
|
]);
|
|
550
|
-
const
|
|
606
|
+
const handleAbortJob = useCallback((job) => {
|
|
607
|
+
if (!job.id || !job.batchId) {
|
|
608
|
+
toast.error("This translation job cannot be aborted.");
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const jobId = job.id;
|
|
612
|
+
const batchId = job.batchId;
|
|
613
|
+
const targetLabel = job.targetLanguage
|
|
614
|
+
? `${job.targetLanguage} translation`
|
|
615
|
+
: "translation";
|
|
551
616
|
editContext?.confirm({
|
|
552
|
-
header: "
|
|
553
|
-
message:
|
|
554
|
-
acceptLabel: "
|
|
617
|
+
header: "Abort Translation Job",
|
|
618
|
+
message: `Abort this ${targetLabel} job? This stops the remaining translation work for this language, but it does not roll back content that was already written.`,
|
|
619
|
+
acceptLabel: "Abort",
|
|
555
620
|
showCancel: true,
|
|
556
621
|
accept: async () => {
|
|
557
|
-
|
|
622
|
+
setAbortingJobIds((prev) => new Set(prev).add(jobId));
|
|
558
623
|
try {
|
|
559
|
-
const result = await
|
|
624
|
+
const result = await abortJob(jobId);
|
|
560
625
|
if (result.type !== "success") {
|
|
561
626
|
toast.error(result.details ||
|
|
562
627
|
result.summary ||
|
|
563
|
-
"Failed to
|
|
628
|
+
"Failed to abort translation job.");
|
|
564
629
|
return;
|
|
565
630
|
}
|
|
566
|
-
toast.success("Translation
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
return next;
|
|
572
|
-
});
|
|
573
|
-
setExpandedBatchId((prev) => (prev === batchId ? null : prev));
|
|
631
|
+
toast.success("Translation job aborted.");
|
|
632
|
+
await loadRecentBatches(0, false, effectiveFilters);
|
|
633
|
+
if (expandedBatchId === batchId || batchJobs[batchId]) {
|
|
634
|
+
await loadBatchJobs(batchId);
|
|
635
|
+
}
|
|
574
636
|
}
|
|
575
637
|
finally {
|
|
576
|
-
|
|
638
|
+
setAbortingJobIds((prev) => {
|
|
577
639
|
const next = new Set(prev);
|
|
578
|
-
next.delete(
|
|
640
|
+
next.delete(jobId);
|
|
579
641
|
return next;
|
|
580
642
|
});
|
|
581
643
|
}
|
|
582
644
|
},
|
|
583
645
|
});
|
|
584
|
-
}, [
|
|
646
|
+
}, [
|
|
647
|
+
batchJobs,
|
|
648
|
+
editContext,
|
|
649
|
+
effectiveFilters,
|
|
650
|
+
expandedBatchId,
|
|
651
|
+
loadBatchJobs,
|
|
652
|
+
loadRecentBatches,
|
|
653
|
+
]);
|
|
585
654
|
const handleRetryJobs = useCallback(async (batchId, provider, jobs, retryKey) => {
|
|
586
655
|
const sessionId = editContext?.sessionId;
|
|
587
656
|
if (!sessionId) {
|
|
@@ -652,67 +721,302 @@ export function TranslationBatches() {
|
|
|
652
721
|
const openItemInEditor = useCallback(async (itemId, language) => {
|
|
653
722
|
const normalized = (itemId ?? "").toString().toLowerCase();
|
|
654
723
|
editContext?.setShowAgentsWorkspaceEditor(true);
|
|
655
|
-
await editContext?.loadItem({ id: normalized, language, version: 0 });
|
|
724
|
+
await editContext?.loadItem(language ? { id: normalized, language, version: 0 } : normalized);
|
|
656
725
|
}, [editContext]);
|
|
657
|
-
|
|
658
|
-
|
|
726
|
+
return (_jsxs("div", { ref: rootRef, className: "flex h-full min-h-0 flex-col bg-neutral-grey-5", style: { padding: isCompactBatches ? 12 : 40 }, "data-testid": "translation-batches", children: [_jsx(PageHeader, { title: "Translation Batches", description: isMobile
|
|
727
|
+
? undefined
|
|
728
|
+
: "Review translation history and manage batch progress", variant: "workspace", className: isCompactBatches ? "mb-4" : "mb-5" }), _jsx("main", { className: "min-h-0 min-w-0 flex-1 overflow-hidden rounded-[12px] bg-white", children: _jsx("div", { className: `h-full min-h-0 overflow-auto ${isCompactBatches ? "p-3" : "p-6"}`, children: isLoading && batches.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32", children: _jsxs("div", { className: "flex items-center gap-2 text-neutral-grey-50", children: [_jsx(LoaderIcon, { className: "h-5 w-5 animate-spin text-highlight-100" }), "Loading recent translations..."] }) })) : groupedBatches.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-32", children: _jsxs("div", { className: "text-center text-neutral-grey-50", children: [_jsx(LanguagesIcon, { className: "h-8 w-8 block mx-auto mb-2 text-neutral-grey-15" }), _jsx("p", { className: "font-medium text-neutral-grey-100", children: "No translations found" }), _jsx("p", { className: "text-sm mt-1", children: (() => {
|
|
729
|
+
const hasActiveFilters = filters.dateRange !== "last30days" ||
|
|
730
|
+
filters.status !== "all" ||
|
|
731
|
+
filters.user !== currentUserName ||
|
|
732
|
+
filters.provider !== "all" ||
|
|
733
|
+
filters.targetLanguage !== "all" ||
|
|
734
|
+
filters.itemIdOrPath.trim() !== "" ||
|
|
735
|
+
filters.itemIncludeSubitems;
|
|
736
|
+
return hasActiveFilters
|
|
737
|
+
? "Try adjusting your filters or start a translation to see it appear here"
|
|
738
|
+
: "Start a translation to see it appear here";
|
|
739
|
+
})() })] }) })) : (_jsxs("div", { className: isCompactBatches ? "space-y-6" : "space-y-10", children: [groupedBatches.map((dateGroup) => (_jsxs("div", { children: [_jsxs("div", { className: "flex items-baseline gap-2 mb-2", children: [_jsx("h2", { className: "text-[11px] font-medium tracking-[0.08em] text-neutral-grey-50", children: dateGroup.label }), _jsx("span", { className: "flex-1 ml-2 border-t border-border-default" })] }), _jsx("div", { className: "divide-border-default divide-y overflow-hidden rounded-lg bg-white", children: dateGroup.batches.map((b) => {
|
|
740
|
+
const batchDate = new Date(b.timestamp);
|
|
741
|
+
const formattedBatchDate = formatDateTime(batchDate);
|
|
742
|
+
const info = b.info;
|
|
743
|
+
const totalJobs = info?.expectedJobs ?? 0;
|
|
744
|
+
const itemsCount = typeof info?.itemsCount === "number"
|
|
745
|
+
? info.itemsCount
|
|
746
|
+
: undefined;
|
|
747
|
+
const languagesCsv = info?.languages;
|
|
748
|
+
const languages = languagesCsv
|
|
749
|
+
? languagesCsv.split(",")
|
|
750
|
+
: [];
|
|
751
|
+
const completedJobs = info?.completedJobs ?? 0;
|
|
752
|
+
const errorJobs = info?.errorJobs ?? 0;
|
|
753
|
+
const anyError = errorJobs > 0;
|
|
754
|
+
const anyInProgress = isActiveBatchStatus(info?.status);
|
|
755
|
+
const anyAborted = info?.status === "Aborted";
|
|
756
|
+
const progressPct = totalJobs > 0
|
|
757
|
+
? Math.min(100, Math.round((completedJobs / totalJobs) * 100))
|
|
758
|
+
: 0;
|
|
759
|
+
const progressTone = anyError
|
|
760
|
+
? "error"
|
|
761
|
+
: anyAborted
|
|
762
|
+
? "aborted"
|
|
763
|
+
: progressPct >= 100
|
|
764
|
+
? "complete"
|
|
765
|
+
: "active";
|
|
766
|
+
const metaParts = [];
|
|
767
|
+
metaParts.push(_jsxs("span", { className: "text-neutral-grey-100", children: [totalJobs, " translation", totalJobs !== 1 ? "s" : ""] }, "trans"));
|
|
768
|
+
if (typeof itemsCount === "number") {
|
|
769
|
+
metaParts.push(_jsxs("span", { children: [itemsCount, " item", itemsCount !== 1 ? "s" : ""] }, "items"));
|
|
770
|
+
}
|
|
771
|
+
if (languages.length > 0) {
|
|
772
|
+
metaParts.push(_jsxs("span", { title: languages.join(", "), children: [languages.length, " language", languages.length !== 1 ? "s" : ""] }, "langs"));
|
|
773
|
+
}
|
|
774
|
+
if (info?.provider) {
|
|
775
|
+
metaParts.push(_jsx("span", { children: info.provider }, "prov"));
|
|
776
|
+
}
|
|
777
|
+
if (info?.totalCost != null) {
|
|
778
|
+
metaParts.push(_jsxs("span", { children: ["Cost:", " ", _jsx("span", { className: "font-medium text-neutral-grey-100", children: formatUsdCost(info.totalCost) })] }, "cost"));
|
|
779
|
+
}
|
|
780
|
+
const ownerPart = info?.initiatedByUser ? (_jsx("span", { title: info.initiatedByUser, children: userDisplayNames[info.initiatedByUser] ||
|
|
781
|
+
info.initiatedByUser }, "user")) : null;
|
|
782
|
+
const datePart = info?.lastUpdatedUtc ? (_jsx("span", { children: formatDateTime(new Date(info.lastUpdatedUtc)) }, "date")) : null;
|
|
783
|
+
const desktopMetaParts = [...metaParts];
|
|
784
|
+
if (!isCompactBatches) {
|
|
785
|
+
if (ownerPart)
|
|
786
|
+
desktopMetaParts.push(ownerPart);
|
|
787
|
+
if (datePart)
|
|
788
|
+
desktopMetaParts.push(datePart);
|
|
789
|
+
}
|
|
790
|
+
const mobileFooterMetaParts = [];
|
|
791
|
+
if (isCompactBatches) {
|
|
792
|
+
if (datePart)
|
|
793
|
+
mobileFooterMetaParts.push(datePart);
|
|
794
|
+
if (ownerPart)
|
|
795
|
+
mobileFooterMetaParts.push(ownerPart);
|
|
796
|
+
}
|
|
797
|
+
const batchDisplayName = (() => {
|
|
798
|
+
let parsedName;
|
|
799
|
+
if (info?.metadata) {
|
|
800
|
+
try {
|
|
801
|
+
const m = typeof info.metadata === "string"
|
|
802
|
+
? JSON.parse(info.metadata)
|
|
803
|
+
: info.metadata;
|
|
804
|
+
const candidate = m?.name;
|
|
805
|
+
if (typeof candidate === "string" &&
|
|
806
|
+
candidate.trim()) {
|
|
807
|
+
parsedName = candidate.trim();
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
catch {
|
|
811
|
+
// Metadata isn't JSON - fall through to timestamp.
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return (parsedName ??
|
|
815
|
+
`Translation Batch · ${formattedBatchDate}`);
|
|
816
|
+
})();
|
|
817
|
+
const isExpanded = expandedBatchId === b.id;
|
|
818
|
+
const jobs = batchJobs[b.id];
|
|
819
|
+
const isLoadingThis = loadingJobs.has(b.id);
|
|
820
|
+
const canRetry = anyError;
|
|
821
|
+
const canAbort = isActiveBatchStatus(info?.status);
|
|
822
|
+
const isRetryingBatch = retryingKeys.has(`batch:${b.id}`);
|
|
823
|
+
const isAborting = abortingBatchIds.has(b.id);
|
|
824
|
+
const hasBatchActions = canRetry || canAbort;
|
|
825
|
+
// Stable test-id status used by Playwright assertions.
|
|
826
|
+
// Matches the visual status dot/spinner logic above.
|
|
827
|
+
const testStatus = anyError
|
|
828
|
+
? "error"
|
|
829
|
+
: anyInProgress
|
|
830
|
+
? "in-progress"
|
|
831
|
+
: anyAborted
|
|
832
|
+
? "aborted"
|
|
833
|
+
: "completed";
|
|
834
|
+
const batchActions = (_jsxs(_Fragment, { children: [canRetry && (_jsx(SimpleIconButton, { onClick: (event) => {
|
|
835
|
+
event.stopPropagation();
|
|
836
|
+
if (!canRetry)
|
|
837
|
+
return;
|
|
838
|
+
void handleRetryBatch(info);
|
|
839
|
+
}, icon: _jsx(RetryIcon, { className: `h-3.5 w-3.5 ${isRetryingBatch ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Retry failed translations", disabled: !canRetry ||
|
|
840
|
+
isRetryingBatch ||
|
|
841
|
+
isLoadingThis ||
|
|
842
|
+
isAborting })), canAbort && (_jsx(SimpleIconButton, { onClick: (event) => {
|
|
843
|
+
event.stopPropagation();
|
|
844
|
+
if (!canAbort)
|
|
845
|
+
return;
|
|
846
|
+
handleAbortBatch(b.id);
|
|
847
|
+
}, icon: _jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Abort translation batch", disabled: !canAbort || isAborting }))] }));
|
|
848
|
+
return (_jsxs("div", { "data-testid": `batch-row-${b.id}`, "data-batch-status": testStatus, children: [_jsx("div", { role: "button", tabIndex: 0, className: `group w-full cursor-pointer py-3 text-left transition-colors hover:bg-neutral-grey-5 ${isCompactBatches ? "px-2.5" : "px-4"}`, onClick: () => toggleBatch(b.id), onKeyDown: (e) => {
|
|
849
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
850
|
+
e.preventDefault();
|
|
851
|
+
toggleBatch(b.id);
|
|
852
|
+
}
|
|
853
|
+
}, children: _jsxs("div", { className: `flex items-start ${isCompactBatches ? "gap-2" : "gap-4"}`, children: [_jsx("div", { className: `shrink-0 text-neutral-grey-100 transition-transform ${isCompactBatches ? "mt-0.5" : "mt-3"} ${isExpanded ? "rotate-180" : ""}`, children: _jsx(ChevronDownIcon, { className: "h-4 w-4" }) }), !isCompactBatches && totalJobs > 0 && (_jsx(BatchProgressIndicator, { value: progressPct, completedJobs: completedJobs, totalJobs: totalJobs, errorJobs: errorJobs, tone: progressTone, testId: `batch-progress-${b.id}` })), _jsxs("div", { className: `flex min-w-0 flex-1 ${isCompactBatches
|
|
854
|
+
? "flex-col gap-2"
|
|
855
|
+
: "items-center justify-between gap-3"}`, children: [_jsxs("div", { className: "grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)]", children: [_jsx("span", { className: `min-w-0 text-[14px] font-medium text-neutral-grey-100 ${isCompactBatches
|
|
856
|
+
? "whitespace-normal break-words leading-5"
|
|
857
|
+
: "truncate"}`, style: isCompactBatches
|
|
858
|
+
? { overflowWrap: "anywhere" }
|
|
859
|
+
: undefined, title: formattedBatchDate, children: batchDisplayName }), _jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: desktopMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, i))) }), mobileFooterMetaParts.length > 0 && (_jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: mobileFooterMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, `footer-${i}`))) }))] }), isCompactBatches && (_jsxs("div", { className: "mt-2 flex min-w-0 items-center gap-2", children: [totalJobs > 0 && (_jsx("div", { className: "min-w-0 flex-1", children: _jsx(BatchProgressIndicator, { value: progressPct, completedJobs: completedJobs, totalJobs: totalJobs, errorJobs: errorJobs, tone: progressTone, testId: `batch-progress-${b.id}`, variant: "bar" }) })), hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: batchActions }))] })), !isCompactBatches && hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-2 self-center", children: batchActions }))] })] }) }), isExpanded && (_jsxs("div", { className: "border-t border-border-default/50", children: [_jsx(CustomPromptPanel, { prompts: parseCustomPrompts(info?.metadata) }), _jsx(BatchItemList, { batchId: b.id, batchProvider: info?.provider, jobs: jobs, isLoading: isLoadingThis, expandedItems: expandedItems, languages: sitecoreLanguages, currentLanguage: editContext?.item?.language, itemFilter: filters.itemIdOrPath.trim(), itemIncludeSubitems: filters.itemIncludeSubitems, statusFilter: filters.status, batchIsTerminal: isTerminalBatchStatus(info?.status), batchStartedAtUtc: info?.startedAtUtc ?? info?.createdAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys, isMobile: isMobile, onToggleItem: toggleItem, onOpenItem: openItemInEditor, onAbortJob: handleAbortJob, onRetryJobs: handleRetryJobs })] }))] }, b.id));
|
|
860
|
+
}) })] }, dateGroup.label))), hasMore && (_jsx("div", { className: "flex justify-center pt-6", children: _jsx(Button, { variant: "outline", size: "sm", onClick: loadMore, disabled: isLoadingMore, children: isLoadingMore ? (_jsxs(_Fragment, { children: [_jsx(LoaderIcon, { className: "h-4 w-4 animate-spin text-highlight-100" }), "Loading more..."] })) : (_jsxs(_Fragment, { children: [_jsx(ChevronDownIcon, { className: "h-4 w-4" }), "Load More Batches"] })) }) }))] })) }) })] }));
|
|
861
|
+
}
|
|
862
|
+
export const TRANSLATION_BATCH_FILTERS_SIDEBAR_ID = "translation-batch-filters";
|
|
863
|
+
export function TranslationBatchFiltersSidebar() {
|
|
864
|
+
const editContext = useEditContext();
|
|
865
|
+
const { filters, itemIdOrPathInput } = useTranslationBatchFilterSnapshot();
|
|
866
|
+
const [providers, setProviders] = useState([]);
|
|
867
|
+
const [sitecoreLanguages, setSitecoreLanguages] = useState([]);
|
|
868
|
+
const [itemPickerOpen, setItemPickerOpen] = useState(false);
|
|
869
|
+
const currentUserName = editContext?.user?.name || "all";
|
|
870
|
+
const isLimitedPreviewUser = editContext?.user?.isLimitedPreviewUser ?? false;
|
|
871
|
+
useEffect(() => {
|
|
872
|
+
ensureDefaultTranslationBatchUser(currentUserName);
|
|
873
|
+
}, [currentUserName]);
|
|
874
|
+
useEffect(() => {
|
|
875
|
+
if (isLimitedPreviewUser &&
|
|
876
|
+
currentUserName !== "all" &&
|
|
877
|
+
filters.user !== currentUserName) {
|
|
878
|
+
setTranslationBatchFilters((prev) => ({
|
|
879
|
+
...prev,
|
|
880
|
+
user: currentUserName,
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
}, [isLimitedPreviewUser, currentUserName, filters.user]);
|
|
884
|
+
const searchTranslationUsers = useCallback((query, limit) => searchBackendUsers(query, limit), []);
|
|
885
|
+
const debouncedSetItemFilter = useDebouncedCallback((value) => setTranslationBatchFilters((prev) => ({
|
|
886
|
+
...prev,
|
|
887
|
+
itemIdOrPath: value,
|
|
888
|
+
})), 400);
|
|
889
|
+
useEffect(() => {
|
|
890
|
+
const loadProviders = async () => {
|
|
891
|
+
try {
|
|
892
|
+
const res = await getTranslationProviders();
|
|
893
|
+
const maybeProviders = res?.data ?? res;
|
|
894
|
+
setProviders(Array.isArray(maybeProviders)
|
|
895
|
+
? maybeProviders
|
|
896
|
+
: []);
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
console.error("Failed to load translation providers:", error);
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
loadProviders();
|
|
903
|
+
}, []);
|
|
904
|
+
useEffect(() => {
|
|
905
|
+
const loadLanguages = async () => {
|
|
906
|
+
try {
|
|
907
|
+
const res = await getLanguages();
|
|
908
|
+
const langs = (res?.data ?? res ?? []);
|
|
909
|
+
setSitecoreLanguages(Array.isArray(langs) ? langs : []);
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
console.error("Failed to load Sitecore languages:", error);
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
loadLanguages();
|
|
916
|
+
}, []);
|
|
917
|
+
const languageOptions = useMemo(() => sitecoreLanguages
|
|
918
|
+
.map((language) => {
|
|
919
|
+
const code = language.languageCode || language.name;
|
|
920
|
+
return code ? { ...language, languageCode: code } : null;
|
|
921
|
+
})
|
|
922
|
+
.filter((language) => language !== null)
|
|
923
|
+
.sort((a, b) => a.languageCode.localeCompare(b.languageCode)), [sitecoreLanguages]);
|
|
924
|
+
const currentItem = editContext?.item;
|
|
925
|
+
const multiItemEnabled = editContext?.configuration?.localization?.multiItem !== false;
|
|
926
|
+
const canStartTranslation = !!editContext && (multiItemEnabled || !!currentItem);
|
|
927
|
+
const handleStartTranslation = useCallback(() => {
|
|
928
|
+
if (!editContext)
|
|
929
|
+
return;
|
|
930
|
+
void openLocalizeItemDialog({
|
|
931
|
+
editContext,
|
|
932
|
+
items: currentItem ? [currentItem] : [],
|
|
933
|
+
});
|
|
934
|
+
}, [currentItem, editContext]);
|
|
659
935
|
const hasActiveFilters = filters.dateRange !== "last30days" ||
|
|
660
936
|
filters.status !== "all" ||
|
|
661
937
|
filters.user !== currentUserName ||
|
|
662
938
|
filters.provider !== "all" ||
|
|
663
939
|
filters.targetLanguage !== "all" ||
|
|
664
|
-
filters.itemIdOrPath.trim() !== ""
|
|
665
|
-
|
|
666
|
-
|
|
940
|
+
filters.itemIdOrPath.trim() !== "" ||
|
|
941
|
+
filters.itemIncludeSubitems;
|
|
942
|
+
const resetFilters = useCallback(() => {
|
|
943
|
+
debouncedSetItemFilter.cancel();
|
|
944
|
+
setTranslationBatchItemInput("");
|
|
945
|
+
setTranslationBatchFilters({
|
|
946
|
+
...DEFAULT_FILTERS,
|
|
947
|
+
user: currentUserName,
|
|
948
|
+
});
|
|
949
|
+
}, [currentUserName, debouncedSetItemFilter]);
|
|
950
|
+
const filterLabelClass = "text-[12px] font-medium text-neutral-grey-50";
|
|
951
|
+
const filterFieldClass = "flex flex-col gap-1";
|
|
952
|
+
return (_jsxs("div", { className: "flex h-full min-h-0 flex-col bg-white", "data-testid": "translation-filters-sidebar", children: [_jsx("div", { className: "border-border-default flex shrink-0 flex-col gap-2 border-b px-3 py-3", children: _jsxs(Button, { type: "button", variant: "outline", size: "lg", title: canStartTranslation
|
|
953
|
+
? "Start new translation"
|
|
954
|
+
: "Open an item to translate", "data-testid": "translation-sidebar-start-new-translation-button", disabled: !canStartTranslation, onClick: handleStartTranslation, className: "border-border-default hover:border-neutral-grey-100 hover:bg-neutral-grey-5 h-9 w-full justify-center gap-3 rounded-md bg-white px-3 text-sm font-medium shadow-none", children: [_jsx("span", { className: "truncate", children: "Start new translation" }), _jsx(PlusIcon, { className: "size-4", strokeWidth: 1.5 })] }) }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 py-4", children: _jsxs("div", { className: "grid gap-4", children: [_jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Date Range" }), _jsx(Select, { className: "w-full bg-white", options: DATE_RANGE_OPTIONS, value: filters.dateRange, onValueChange: (value) => setTranslationBatchFilters((prev) => ({
|
|
667
955
|
...prev,
|
|
668
956
|
dateRange: value,
|
|
669
|
-
})), placeholder: "Select date range" })] }), _jsxs("div", { className:
|
|
957
|
+
})), placeholder: "Select date range" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Status" }), _jsx(Select, { className: "w-full bg-white", options: STATUS_OPTIONS, value: filters.status, onValueChange: (value) => setTranslationBatchFilters((prev) => ({
|
|
670
958
|
...prev,
|
|
671
959
|
status: value,
|
|
672
|
-
})), placeholder: "Select status" })] }), _jsxs("div", { className:
|
|
960
|
+
})), placeholder: "Select status" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "User" }), _jsx(UserPicker, { value: isLimitedPreviewUser
|
|
673
961
|
? currentUserName
|
|
674
962
|
: filters.user === "all"
|
|
675
963
|
? null
|
|
676
964
|
: filters.user, displayValue: isLimitedPreviewUser || filters.user === currentUserName
|
|
677
965
|
? `${editContext?.user?.fullName?.trim() || currentUserName} (You)`
|
|
678
|
-
: undefined,
|
|
966
|
+
: undefined, displayAvatarName: isLimitedPreviewUser || filters.user === currentUserName
|
|
967
|
+
? editContext?.user?.fullName?.trim() || currentUserName
|
|
968
|
+
: undefined, displayAvatarUrl: isLimitedPreviewUser || filters.user === currentUserName
|
|
969
|
+
? editContext?.user?.avatarUrl
|
|
970
|
+
: undefined, onChange: (user) => setTranslationBatchFilters((prev) => ({
|
|
679
971
|
...prev,
|
|
680
972
|
user: user?.userName ?? "all",
|
|
681
|
-
})), disabled: isLimitedPreviewUser, placeholder: "All Users", buttonClassName: "h-
|
|
973
|
+
})), disabled: isLimitedPreviewUser, placeholder: "All Users", buttonClassName: "h-8 min-h-0 w-full justify-start rounded-[4px] border bg-white px-2 py-0 text-xs font-medium", allowClear: true, clearLabel: "All Users", showIcon: isLimitedPreviewUser || filters.user !== "all", searchUsers: searchTranslationUsers })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Provider" }), _jsx(Select, { className: "w-full bg-white", options: [
|
|
682
974
|
{ value: "all", label: "All Providers" },
|
|
683
975
|
...providers.map((p) => ({
|
|
684
976
|
value: p.name,
|
|
685
977
|
label: p.displayName || p.name,
|
|
686
978
|
})),
|
|
687
|
-
], value: filters.provider, onValueChange: (value) =>
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
979
|
+
], value: filters.provider, onValueChange: (value) => setTranslationBatchFilters((prev) => ({
|
|
980
|
+
...prev,
|
|
981
|
+
provider: value,
|
|
982
|
+
})), placeholder: "Select provider" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Target Language" }), _jsx(Select, { className: "w-full bg-white", options: [
|
|
983
|
+
{
|
|
984
|
+
value: "all",
|
|
985
|
+
label: "All Languages",
|
|
986
|
+
},
|
|
987
|
+
...languageOptions.map((language) => ({
|
|
988
|
+
value: language.languageCode,
|
|
989
|
+
label: language.languageCode,
|
|
990
|
+
icon: renderTargetLanguageIcon(language),
|
|
692
991
|
})),
|
|
693
|
-
], value: filters.targetLanguage, onValueChange: (value) =>
|
|
992
|
+
], value: filters.targetLanguage, onValueChange: (value) => setTranslationBatchFilters((prev) => ({
|
|
993
|
+
...prev,
|
|
994
|
+
targetLanguage: value,
|
|
995
|
+
})), placeholder: "Select language" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("label", { className: filterLabelClass, children: "Item ID / Path" }), _jsxs("label", { className: "flex cursor-pointer items-center gap-1 text-[10px] text-neutral-grey-50 select-none", children: [_jsx("input", { type: "checkbox", checked: filters.itemIncludeSubitems, onChange: (e) => setTranslationBatchFilters((prev) => ({
|
|
694
996
|
...prev,
|
|
695
997
|
itemIncludeSubitems: e.target.checked,
|
|
696
|
-
})), className: "h-3 w-3 accent-highlight-100
|
|
998
|
+
})), className: "h-3 w-3 cursor-pointer accent-highlight-100" }), "Include subitems"] })] }), _jsxs("div", { className: "relative", children: [_jsx(Input, { value: itemIdOrPathInput, onChange: (e) => {
|
|
697
999
|
const next = e.target.value;
|
|
698
|
-
|
|
1000
|
+
setTranslationBatchItemInput(next);
|
|
699
1001
|
debouncedSetItemFilter(next);
|
|
700
|
-
}, placeholder: "GUID or /sitecore/content
|
|
701
|
-
|
|
1002
|
+
}, placeholder: "GUID or /sitecore/content/...", className: "h-auto min-h-[34px] w-full bg-white py-2 pr-14 text-xs" }), _jsxs("div", { className: "absolute top-1/2 right-1.5 flex -translate-y-1/2 items-center gap-1", children: [itemIdOrPathInput && (_jsx("button", { type: "button", onClick: () => {
|
|
1003
|
+
setTranslationBatchItemInput("");
|
|
702
1004
|
debouncedSetItemFilter.cancel();
|
|
703
|
-
|
|
1005
|
+
setTranslationBatchFilters((prev) => ({
|
|
704
1006
|
...prev,
|
|
705
1007
|
itemIdOrPath: "",
|
|
706
1008
|
itemIncludeSubitems: false,
|
|
707
1009
|
}));
|
|
708
|
-
}, className: "text-neutral-grey-50 hover:text-neutral-grey-100
|
|
1010
|
+
}, className: "cursor-pointer text-neutral-grey-50 hover:text-neutral-grey-100", "aria-label": "Clear", children: _jsx(XIcon, { className: "h-3.5 w-3.5" }) })), _jsxs(Popover, { open: itemPickerOpen, onOpenChange: setItemPickerOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx("button", { type: "button", className: "cursor-pointer text-neutral-grey-50 hover:text-neutral-grey-100", "aria-label": "Browse content tree", title: "Browse content tree", children: _jsx(FolderTreeIcon, { className: "h-3.5 w-3.5" }) }) }), _jsxs(PopoverContent, { align: "end", sideOffset: 6, className: "w-[28rem] max-w-[calc(100vw-2rem)] p-0", children: [_jsx("div", { className: "flex items-center justify-between gap-2 border-b border-border-default px-3 py-2 text-[11px] text-neutral-grey-50", children: _jsx("span", { children: "Pick item to filter by" }) }), _jsx("div", { className: "max-h-80 overflow-auto p-2", children: _jsx(ContentTree, { language: "en", selectionMode: "single", rootItemIds: ["0de95ae4-41ab-4d01-9eb0-67441b7c2450"], initialExpandedKeys: [
|
|
1011
|
+
"0de95ae4-41ab-4d01-9eb0-67441b7c2450",
|
|
1012
|
+
], onSelectionChange: (items) => {
|
|
709
1013
|
const picked = items?.[0];
|
|
710
1014
|
if (!picked)
|
|
711
1015
|
return;
|
|
712
1016
|
const applyValue = (value) => {
|
|
713
|
-
|
|
1017
|
+
setTranslationBatchItemInput(value);
|
|
714
1018
|
debouncedSetItemFilter.cancel();
|
|
715
|
-
|
|
1019
|
+
setTranslationBatchFilters((prev) => ({
|
|
716
1020
|
...prev,
|
|
717
1021
|
itemIdOrPath: value,
|
|
718
1022
|
}));
|
|
@@ -722,134 +1026,19 @@ export function TranslationBatches() {
|
|
|
722
1026
|
applyValue(picked.path);
|
|
723
1027
|
return;
|
|
724
1028
|
}
|
|
725
|
-
// Fall back: resolve the item's path via a stub fetch
|
|
726
|
-
// so the input shows a human-readable path instead of a GUID.
|
|
727
1029
|
fetchItemStubs([
|
|
728
|
-
{
|
|
1030
|
+
{
|
|
1031
|
+
id: picked.id,
|
|
1032
|
+
language: "en",
|
|
1033
|
+
version: 1,
|
|
1034
|
+
},
|
|
729
1035
|
])
|
|
730
1036
|
.then((stubs) => {
|
|
731
1037
|
const stubPath = stubs?.[0]?.path;
|
|
732
1038
|
applyValue(stubPath || picked.id);
|
|
733
1039
|
})
|
|
734
1040
|
.catch(() => applyValue(picked.id));
|
|
735
|
-
} }) })] })] })] })] })] })
|
|
736
|
-
const hasActiveFilters = filters.dateRange !== "last30days" ||
|
|
737
|
-
filters.status !== "all" ||
|
|
738
|
-
filters.user !== currentUserName ||
|
|
739
|
-
filters.provider !== "all" ||
|
|
740
|
-
filters.targetLanguage !== "all" ||
|
|
741
|
-
filters.itemIdOrPath.trim() !== "";
|
|
742
|
-
return hasActiveFilters
|
|
743
|
-
? "Try adjusting your filters or start a translation to see it appear here"
|
|
744
|
-
: "Start a translation to see it appear here";
|
|
745
|
-
})() })] }) })) : (_jsxs("div", { className: "space-y-10", children: [groupedBatches.map((dateGroup) => (_jsxs("div", { children: [_jsxs("div", { className: "flex items-baseline gap-2 mb-2", children: [_jsx("h2", { className: "text-[11px] font-medium tracking-[0.08em] text-neutral-grey-50", children: dateGroup.label }), _jsx("span", { className: "flex-1 ml-2 border-t border-border-default" })] }), _jsx("div", { className: "divide-y divide-border-default/70", children: dateGroup.batches.map((b) => {
|
|
746
|
-
const batchDate = new Date(b.timestamp);
|
|
747
|
-
const now = new Date();
|
|
748
|
-
const isToday = batchDate.toDateString() === now.toDateString();
|
|
749
|
-
const timeDisplay = isToday
|
|
750
|
-
? batchDate.toLocaleTimeString()
|
|
751
|
-
: batchDate.toLocaleDateString() +
|
|
752
|
-
" " +
|
|
753
|
-
batchDate.toLocaleTimeString();
|
|
754
|
-
const info = b.info;
|
|
755
|
-
const totalJobs = info?.expectedJobs ?? 0;
|
|
756
|
-
const itemsCount = typeof info?.itemsCount === "number"
|
|
757
|
-
? info.itemsCount
|
|
758
|
-
: undefined;
|
|
759
|
-
const languagesCsv = info?.languages;
|
|
760
|
-
const languages = languagesCsv
|
|
761
|
-
? languagesCsv.split(",")
|
|
762
|
-
: [];
|
|
763
|
-
const completedJobs = info?.completedJobs ?? 0;
|
|
764
|
-
const errorJobs = info?.errorJobs ?? 0;
|
|
765
|
-
const anyError = errorJobs > 0;
|
|
766
|
-
const anyInProgress = isActiveBatchStatus(info?.status);
|
|
767
|
-
const anyAborted = info?.status === "Aborted";
|
|
768
|
-
const progressPct = totalJobs > 0
|
|
769
|
-
? Math.min(100, Math.round((completedJobs / totalJobs) * 100))
|
|
770
|
-
: 0;
|
|
771
|
-
const showProgress = totalJobs > 0 && (anyInProgress || anyError);
|
|
772
|
-
const statusDotClass = anyError
|
|
773
|
-
? "bg-feedback-red"
|
|
774
|
-
: anyInProgress
|
|
775
|
-
? "bg-highlight-100"
|
|
776
|
-
: anyAborted
|
|
777
|
-
? "bg-feedback-orange"
|
|
778
|
-
: "bg-feedback-green/70";
|
|
779
|
-
const metaParts = [];
|
|
780
|
-
metaParts.push(_jsxs("span", { className: "text-neutral-grey-100", children: [totalJobs, " translation", totalJobs !== 1 ? "s" : ""] }, "trans"));
|
|
781
|
-
if (typeof itemsCount === "number") {
|
|
782
|
-
metaParts.push(_jsxs("span", { children: [itemsCount, " item", itemsCount !== 1 ? "s" : ""] }, "items"));
|
|
783
|
-
}
|
|
784
|
-
if (languages.length > 0) {
|
|
785
|
-
metaParts.push(_jsxs("span", { title: languages.join(", "), children: [languages.length, " language", languages.length !== 1 ? "s" : ""] }, "langs"));
|
|
786
|
-
}
|
|
787
|
-
if (info?.provider) {
|
|
788
|
-
metaParts.push(_jsx("span", { children: info.provider }, "prov"));
|
|
789
|
-
}
|
|
790
|
-
if (info?.totalCost != null) {
|
|
791
|
-
metaParts.push(_jsxs("span", { children: ["Cost:", " ", _jsx("span", { className: "font-medium text-neutral-grey-100", children: formatUsdCost(info.totalCost) })] }, "cost"));
|
|
792
|
-
}
|
|
793
|
-
if (info?.initiatedByUser) {
|
|
794
|
-
const resolved = userDisplayNames[info.initiatedByUser];
|
|
795
|
-
metaParts.push(_jsx("span", { title: info.initiatedByUser, children: resolved || info.initiatedByUser }, "user"));
|
|
796
|
-
}
|
|
797
|
-
const isExpanded = expandedBatchId === b.id;
|
|
798
|
-
const jobs = batchJobs[b.id];
|
|
799
|
-
const isLoadingThis = loadingJobs.has(b.id);
|
|
800
|
-
const canRetry = anyError;
|
|
801
|
-
const canAbort = isActiveBatchStatus(info?.status);
|
|
802
|
-
const canDelete = isTerminalBatchStatus(info?.status);
|
|
803
|
-
const isRetryingBatch = retryingKeys.has(`batch:${b.id}`);
|
|
804
|
-
const isAborting = abortingBatchIds.has(b.id);
|
|
805
|
-
const isDeleting = deletingBatchIds.has(b.id);
|
|
806
|
-
// Stable test-id status used by Playwright assertions.
|
|
807
|
-
// Matches the visual status dot/spinner logic above.
|
|
808
|
-
const testStatus = anyError
|
|
809
|
-
? "error"
|
|
810
|
-
: anyInProgress
|
|
811
|
-
? "in-progress"
|
|
812
|
-
: anyAborted
|
|
813
|
-
? "aborted"
|
|
814
|
-
: "completed";
|
|
815
|
-
return (_jsxs("div", { "data-testid": `batch-row-${b.id}`, "data-batch-status": testStatus, children: [_jsx("div", { role: "button", tabIndex: 0, className: "group w-full text-left px-3 md:px-4 py-3 hover:bg-neutral-grey-5 transition-colors cursor-pointer", onClick: () => toggleBatch(b.id), onKeyDown: (e) => {
|
|
816
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
817
|
-
e.preventDefault();
|
|
818
|
-
toggleBatch(b.id);
|
|
819
|
-
}
|
|
820
|
-
}, children: _jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2.5", children: [anyInProgress ? (_jsx(LoaderIcon, { className: "h-3 w-3 shrink-0 animate-spin text-highlight-100" })) : (_jsx("span", { className: `h-1.5 w-1.5 shrink-0 rounded-full ${statusDotClass}`, "aria-hidden": "true" })), _jsx("span", { className: "text-neutral-grey-100 truncate", title: timeDisplay, children: (() => {
|
|
821
|
-
let parsedName;
|
|
822
|
-
if (info?.metadata) {
|
|
823
|
-
try {
|
|
824
|
-
const m = typeof info.metadata === "string"
|
|
825
|
-
? JSON.parse(info.metadata)
|
|
826
|
-
: info.metadata;
|
|
827
|
-
const candidate = m?.name;
|
|
828
|
-
if (typeof candidate === "string" &&
|
|
829
|
-
candidate.trim()) {
|
|
830
|
-
parsedName = candidate.trim();
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
catch {
|
|
834
|
-
// Metadata isn't JSON — fall through to timestamp.
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
return (parsedName ??
|
|
838
|
-
`Translation Batch · ${timeDisplay}`);
|
|
839
|
-
})() }), showProgress && (_jsxs("span", { className: `inline-flex items-center gap-1 text-[11px] tabular-nums ${anyError ? "text-feedback-red" : "text-highlight-100"}`, "data-testid": `batch-progress-${b.id}`, children: [completedJobs, "/", totalJobs, anyError && (_jsxs("span", { className: "text-feedback-red", children: ["\u00B7 ", errorJobs, " error", errorJobs !== 1 ? "s" : ""] }))] }))] }), _jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50 pl-4", children: [metaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, i))), info?.lastUpdatedUtc && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" }), _jsx("span", { children: new Date(info.lastUpdatedUtc).toLocaleString() })] }))] })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [canRetry && (_jsx(SimpleIconButton, { onClick: (event) => {
|
|
840
|
-
event.stopPropagation();
|
|
841
|
-
void handleRetryBatch(info);
|
|
842
|
-
}, icon: _jsx(RetryIcon, { className: `h-3.5 w-3.5 ${isRetryingBatch ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Retry failed translations", disabled: isRetryingBatch ||
|
|
843
|
-
isLoadingThis ||
|
|
844
|
-
isAborting ||
|
|
845
|
-
isDeleting, className: "p-0! text-feedback-red hover:text-feedback-red" })), canAbort && (_jsx(SimpleIconButton, { onClick: (event) => {
|
|
846
|
-
event.stopPropagation();
|
|
847
|
-
handleAbortBatch(b.id);
|
|
848
|
-
}, icon: _jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Abort translation batch", disabled: isAborting || isDeleting, className: "p-0! text-feedback-orange hover:text-feedback-orange" })), canDelete && (_jsx(SimpleIconButton, { onClick: (event) => {
|
|
849
|
-
event.stopPropagation();
|
|
850
|
-
handleDeleteBatch(b.id);
|
|
851
|
-
}, icon: _jsx(DeleteIcon, { size: "md" }), label: "Delete translation history", disabled: isDeleting || isAborting, className: "p-0! text-neutral-grey-50 hover:bg-neutral-grey-5 hover:text-neutral-grey-50" })), _jsx("div", { className: `text-neutral-grey-15 transition-transform ${isExpanded ? "rotate-180" : ""} group-hover:text-neutral-grey-50`, children: _jsx(ChevronDownIcon, { className: "h-4 w-4" }) })] })] }) }), isExpanded && (_jsxs(_Fragment, { children: [_jsx(CustomPromptPanel, { prompts: parseCustomPrompts(info?.metadata) }), _jsx(BatchItemList, { batchId: b.id, batchProvider: info?.provider, jobs: jobs, isLoading: isLoadingThis, expandedItems: expandedItems, languages: sitecoreLanguages, itemFilter: filters.itemIdOrPath.trim(), itemIncludeSubitems: filters.itemIncludeSubitems, statusFilter: filters.status, batchIsTerminal: isTerminalBatchStatus(info?.status), batchStartedAtUtc: info?.startedAtUtc ?? info?.createdAtUtc, retryingKeys: retryingKeys, onToggleItem: toggleItem, onOpenItem: openItemInEditor, onRetryJobs: handleRetryJobs })] }))] }, b.id));
|
|
852
|
-
}) })] }, dateGroup.label))), hasMore && (_jsx("div", { className: "flex justify-center pt-6", children: _jsx(Button, { variant: "outline", size: "sm", onClick: loadMore, disabled: isLoadingMore, children: isLoadingMore ? (_jsxs(_Fragment, { children: [_jsx(LoaderIcon, { className: "h-4 w-4 animate-spin text-highlight-100" }), "Loading more..."] })) : (_jsxs(_Fragment, { children: [_jsx(ChevronDownIcon, { className: "h-4 w-4" }), "Load More Batches"] })) }) }))] })) })] }));
|
|
1041
|
+
} }) })] })] })] })] })] }), _jsx("div", { className: "flex justify-start pt-1", children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: resetFilters, disabled: !hasActiveFilters, children: [_jsx(XIcon, { className: "h-3.5 w-3.5" }), "Reset Filters"] }) })] }) })] }));
|
|
853
1042
|
}
|
|
854
1043
|
function isLikelyGuid(value) {
|
|
855
1044
|
return /^[{(]?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[)}]?$/i.test(value.trim());
|
|
@@ -932,6 +1121,45 @@ function formatUsdCost(value) {
|
|
|
932
1121
|
maximumFractionDigits: value > 0 && value < 0.01 ? 6 : 2,
|
|
933
1122
|
}).format(value);
|
|
934
1123
|
}
|
|
1124
|
+
function BatchProgressIndicator({ value, completedJobs, totalJobs, errorJobs, tone, testId, variant = "circle", }) {
|
|
1125
|
+
const percent = Math.max(0, Math.min(100, Math.round(value)));
|
|
1126
|
+
const radius = 17;
|
|
1127
|
+
const circumference = 2 * Math.PI * radius;
|
|
1128
|
+
const dashOffset = circumference - (percent / 100) * circumference;
|
|
1129
|
+
const hasErrors = errorJobs > 0;
|
|
1130
|
+
const toneClass = tone === "complete"
|
|
1131
|
+
? "text-feedback-green"
|
|
1132
|
+
: tone === "error"
|
|
1133
|
+
? "text-feedback-red"
|
|
1134
|
+
: tone === "aborted"
|
|
1135
|
+
? "text-feedback-orange"
|
|
1136
|
+
: "text-neutral-grey-50";
|
|
1137
|
+
const toneColor = tone === "complete"
|
|
1138
|
+
? "var(--color-feedback-green)"
|
|
1139
|
+
: tone === "error"
|
|
1140
|
+
? "var(--color-feedback-red)"
|
|
1141
|
+
: tone === "aborted"
|
|
1142
|
+
? "var(--color-feedback-orange)"
|
|
1143
|
+
: "var(--color-neutral-grey-50)";
|
|
1144
|
+
const trackColor = tone === "complete"
|
|
1145
|
+
? "var(--color-feedback-green-light)"
|
|
1146
|
+
: tone === "error"
|
|
1147
|
+
? "var(--color-feedback-red-light)"
|
|
1148
|
+
: tone === "aborted"
|
|
1149
|
+
? "var(--color-feedback-orange-light)"
|
|
1150
|
+
: "var(--color-neutral-grey-15)";
|
|
1151
|
+
const label = `${percent}% complete (${completedJobs}/${totalJobs} translations${hasErrors ? `, ${errorJobs} failed` : ""})`;
|
|
1152
|
+
if (variant === "bar") {
|
|
1153
|
+
return (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: "flex min-w-0 items-center gap-2", "aria-label": label, "data-testid": testId, children: [_jsx("div", { className: "h-1.5 min-w-0 flex-1 overflow-hidden rounded-full", style: { backgroundColor: trackColor }, children: _jsx("div", { className: "h-full rounded-full transition-[width]", style: {
|
|
1154
|
+
width: `${percent}%`,
|
|
1155
|
+
backgroundColor: toneColor,
|
|
1156
|
+
} }) }), _jsxs("span", { className: `shrink-0 text-[11px] leading-none font-medium tabular-nums ${toneClass}`, children: [percent, "%"] })] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: label })] }));
|
|
1157
|
+
}
|
|
1158
|
+
return (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: `relative h-10 w-10 shrink-0 rounded-full ${tone === "complete" ? "bg-feedback-green-light" : ""}`, "aria-label": label, "data-testid": testId, children: [_jsxs("svg", { className: "h-10 w-10 -rotate-90", viewBox: "0 0 40 40", children: [_jsx("circle", { cx: "20", cy: "20", r: radius, fill: "none", stroke: trackColor, strokeWidth: "2" }), _jsx("circle", { cx: "20", cy: "20", r: radius, fill: "none", stroke: toneColor, strokeLinecap: "round", strokeWidth: "2", style: {
|
|
1159
|
+
strokeDasharray: circumference,
|
|
1160
|
+
strokeDashoffset: dashOffset,
|
|
1161
|
+
} })] }), _jsxs("span", { className: `absolute inset-0 flex items-center justify-center text-[10px] leading-none font-medium tabular-nums ${toneClass}`, children: [percent, "%"] })] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: label })] }));
|
|
1162
|
+
}
|
|
935
1163
|
function itemStatusDot(jobs) {
|
|
936
1164
|
const hasError = jobs.some((j) => j.status === "Error");
|
|
937
1165
|
const hasInProgress = jobs.some((j) => j.status === "In Progress");
|
|
@@ -947,6 +1175,30 @@ function itemStatusDot(jobs) {
|
|
|
947
1175
|
return { cls: "bg-feedback-orange", label: "Aborted" };
|
|
948
1176
|
return { cls: "bg-feedback-green/70", label: "Completed" };
|
|
949
1177
|
}
|
|
1178
|
+
function InlineLanguageJobChips({ batchId, batchProvider, jobs, languageMap, isExpanded, batchStartedAtUtc, abortingJobIds, retryingKeys, singleLine = false, onToggle, onOpenItem, onAbortJob, onRetryJobs, }) {
|
|
1179
|
+
if (jobs.length === 0)
|
|
1180
|
+
return null;
|
|
1181
|
+
const handleCellClick = (event) => {
|
|
1182
|
+
event.stopPropagation();
|
|
1183
|
+
onToggle();
|
|
1184
|
+
};
|
|
1185
|
+
const handleCellKeyDown = (event) => {
|
|
1186
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1187
|
+
event.preventDefault();
|
|
1188
|
+
event.stopPropagation();
|
|
1189
|
+
onToggle();
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
return (_jsx("div", { role: "button", tabIndex: 0, className: `flex min-w-0 cursor-pointer items-start justify-start gap-1.5 ${singleLine
|
|
1193
|
+
? "flex-nowrap"
|
|
1194
|
+
: isExpanded
|
|
1195
|
+
? "flex-wrap"
|
|
1196
|
+
: "max-h-7 flex-wrap overflow-hidden"}`, "aria-label": `${jobs.length} target language${jobs.length !== 1 ? "s" : ""}`, "aria-expanded": isExpanded, onClick: handleCellClick, onKeyDown: handleCellKeyDown, children: jobs.map((job, idx) => {
|
|
1197
|
+
const lang = languageMap.get((job.targetLanguage || "").toLowerCase());
|
|
1198
|
+
const retryKey = `job:${job.id ?? `${job.itemId}:${job.sourceLanguage}:${job.targetLanguage}`}`;
|
|
1199
|
+
return (_jsx("span", { className: "shrink-0", onClick: (event) => event.stopPropagation(), children: _jsx(LanguageJobChip, { job: job, language: lang, batchStartedAtUtc: batchStartedAtUtc, onOpen: () => onOpenItem(job.itemId, job.targetLanguage), isAborting: !!job.id && abortingJobIds.has(job.id), onAbort: () => onAbortJob(job), isRetrying: retryingKeys.has(retryKey), onRetry: () => onRetryJobs(batchId, batchProvider, [job], retryKey) }) }, `${job.id ?? idx}-${job.targetLanguage}`));
|
|
1200
|
+
}) }));
|
|
1201
|
+
}
|
|
950
1202
|
function CustomPromptPanel({ prompts }) {
|
|
951
1203
|
const [openProvider, setOpenProvider] = React.useState(null);
|
|
952
1204
|
const [copiedProvider, setCopiedProvider] = React.useState(null);
|
|
@@ -971,7 +1223,7 @@ function CustomPromptPanel({ prompts }) {
|
|
|
971
1223
|
toast.error("Failed to copy prompt to clipboard");
|
|
972
1224
|
}
|
|
973
1225
|
};
|
|
974
|
-
return (_jsx("div", { className: "
|
|
1226
|
+
return (_jsx("div", { className: "border-b border-border-default/50", children: prompts.map((p) => {
|
|
975
1227
|
const isOpen = openProvider === p.provider;
|
|
976
1228
|
const justCopied = copiedProvider === p.provider;
|
|
977
1229
|
return (_jsxs("div", { children: [_jsxs("div", { role: "button", tabIndex: 0, style: { paddingLeft: "2rem" }, className: "group flex items-center gap-2 pr-3 md:pr-4 py-2 text-[12px] text-neutral-grey-50 hover:bg-neutral-grey-5 transition-colors cursor-pointer", onClick: () => setOpenProvider(isOpen ? null : p.provider), onKeyDown: (e) => {
|
|
@@ -982,10 +1234,10 @@ function CustomPromptPanel({ prompts }) {
|
|
|
982
1234
|
}, children: [_jsx(SparklesIcon, { className: "h-3 w-3 shrink-0 text-highlight-100", strokeWidth: 1.75 }), _jsx("span", { className: "text-neutral-grey-100", children: "Custom prompt" }), _jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" }), _jsx("span", { children: p.provider }), _jsxs("span", { className: "ml-auto flex items-center gap-2", children: [isOpen && (_jsx(SimpleIconButton, { onClick: (event) => {
|
|
983
1235
|
event.stopPropagation();
|
|
984
1236
|
void handleCopy(p.provider, p.prompt);
|
|
985
|
-
}, icon: justCopied ? (_jsx(CheckIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 })) : (_jsx(CopyIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 })), label: justCopied ? "Copied" : "Copy prompt"
|
|
1237
|
+
}, icon: justCopied ? (_jsx(CheckIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 })) : (_jsx(CopyIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 })), label: justCopied ? "Copied" : "Copy prompt" })), _jsx(ChevronDownIcon, { className: `h-3.5 w-3.5 text-neutral-grey-100 transition-transform ${isOpen ? "rotate-180" : ""}` })] })] }), isOpen && (_jsx("div", { style: { paddingLeft: "2.5rem" }, className: "pr-3 md:pr-4 pb-3", children: _jsx("pre", { className: "max-h-64 overflow-y-auto whitespace-pre-wrap break-words rounded border border-border-default/50 bg-white p-2 font-mono text-[12px] text-neutral-grey-100", children: p.prompt }) }))] }, p.provider));
|
|
986
1238
|
}) }));
|
|
987
1239
|
}
|
|
988
|
-
function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems, languages, itemFilter, itemIncludeSubitems, statusFilter, batchIsTerminal, batchStartedAtUtc, retryingKeys, onToggleItem, onOpenItem, onRetryJobs, }) {
|
|
1240
|
+
function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems, languages, currentLanguage, itemFilter, itemIncludeSubitems, statusFilter, batchIsTerminal, batchStartedAtUtc, abortingJobIds, retryingKeys, isMobile = false, onToggleItem, onOpenItem, onAbortJob, onRetryJobs, }) {
|
|
989
1241
|
const languageMap = React.useMemo(() => {
|
|
990
1242
|
const map = new Map();
|
|
991
1243
|
for (const l of languages) {
|
|
@@ -1066,6 +1318,53 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
|
|
|
1066
1318
|
return [];
|
|
1067
1319
|
return filteredJobs.filter((j) => matchingItemIds.has((j.itemId || "").toString().toLowerCase()));
|
|
1068
1320
|
}, [filteredJobs, statusFilter]);
|
|
1321
|
+
const [stackLanguageChips, setStackLanguageChips] = React.useState(isMobile);
|
|
1322
|
+
const itemTableContainerRef = React.useRef(null);
|
|
1323
|
+
const itemTableRef = React.useRef(null);
|
|
1324
|
+
const normalTableMinWidthRef = React.useRef(null);
|
|
1325
|
+
React.useEffect(() => {
|
|
1326
|
+
const container = itemTableContainerRef.current;
|
|
1327
|
+
const table = itemTableRef.current;
|
|
1328
|
+
if (!container || !table)
|
|
1329
|
+
return;
|
|
1330
|
+
const updateLayout = () => {
|
|
1331
|
+
const availableWidth = container.clientWidth;
|
|
1332
|
+
if (!availableWidth)
|
|
1333
|
+
return;
|
|
1334
|
+
if (isMobile || availableWidth < 640) {
|
|
1335
|
+
setStackLanguageChips(true);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (stackLanguageChips) {
|
|
1339
|
+
const normalMinWidth = normalTableMinWidthRef.current;
|
|
1340
|
+
if (normalMinWidth && availableWidth >= normalMinWidth) {
|
|
1341
|
+
setStackLanguageChips(false);
|
|
1342
|
+
}
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
const requiredWidth = table.scrollWidth;
|
|
1346
|
+
normalTableMinWidthRef.current = requiredWidth;
|
|
1347
|
+
if (requiredWidth > availableWidth + 1) {
|
|
1348
|
+
setStackLanguageChips(true);
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
updateLayout();
|
|
1352
|
+
const animationFrame = window.requestAnimationFrame(updateLayout);
|
|
1353
|
+
const timeout = window.setTimeout(updateLayout, 150);
|
|
1354
|
+
let observer;
|
|
1355
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
1356
|
+
observer = new ResizeObserver(updateLayout);
|
|
1357
|
+
observer.observe(container);
|
|
1358
|
+
observer.observe(table);
|
|
1359
|
+
}
|
|
1360
|
+
window.addEventListener("resize", updateLayout);
|
|
1361
|
+
return () => {
|
|
1362
|
+
window.cancelAnimationFrame(animationFrame);
|
|
1363
|
+
window.clearTimeout(timeout);
|
|
1364
|
+
observer?.disconnect();
|
|
1365
|
+
window.removeEventListener("resize", updateLayout);
|
|
1366
|
+
};
|
|
1367
|
+
}, [isMobile, stackLanguageChips, statusFilteredJobs]);
|
|
1069
1368
|
if (isLoading && !jobs) {
|
|
1070
1369
|
return (_jsxs("div", { className: "px-3 md:px-4 py-4 pl-8 text-[12px] text-neutral-grey-50", children: [_jsx(LoaderIcon, { className: "inline h-3.5 w-3.5 mr-1.5 animate-spin text-highlight-100" }), "Loading items\u2026"] }));
|
|
1071
1370
|
}
|
|
@@ -1083,45 +1382,55 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
|
|
|
1083
1382
|
byItem.set(key, []);
|
|
1084
1383
|
byItem.get(key).push(job);
|
|
1085
1384
|
}
|
|
1086
|
-
return (_jsx("div", { className: "
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1385
|
+
return (_jsx("div", { ref: itemTableContainerRef, className: "w-full overflow-hidden", "data-language-layout": stackLanguageChips ? "stacked" : "columns", children: _jsxs("table", { ref: itemTableRef, className: "w-full border-collapse", children: [_jsx("thead", { children: _jsxs("tr", { children: [!stackLanguageChips && (_jsx("th", { "aria-hidden": "true", className: "px-3 pt-3 pb-1 md:px-4" })), _jsx("th", { scope: "col", className: `pt-3 pb-1 text-left align-top text-[11px] font-medium text-neutral-grey-50 ${stackLanguageChips ? "pr-2 pl-2.5" : "pr-4"}`, children: _jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [_jsx("span", { className: "h-1.5 w-1.5 shrink-0", "aria-hidden": "true" }), _jsx("span", { children: "Pages / Items" })] }) }), !stackLanguageChips && (_jsx("th", { scope: "col", className: "pt-3 pb-1 pr-2 text-left align-top text-[11px] font-medium text-neutral-grey-50", children: "Languages" })), !stackLanguageChips && (_jsx("th", { "aria-hidden": "true", className: "pt-3 pr-3 pb-1 md:pr-4" }))] }) }), _jsx("tbody", { children: Array.from(byItem.entries()).map(([itemId, itemJobs], itemIndex) => {
|
|
1386
|
+
const isItemExpanded = expandedItems.has(itemId);
|
|
1387
|
+
const itemAnyInProgress = itemJobs.some((j) => j.status === "In Progress");
|
|
1388
|
+
const itemAnyPending = itemJobs.some((j) => j.status === "Pending");
|
|
1389
|
+
const itemIsQueued = !itemAnyInProgress && itemAnyPending;
|
|
1390
|
+
const status = itemStatusDot(itemJobs);
|
|
1391
|
+
const firstJob = itemJobs[0];
|
|
1392
|
+
const itemPath = firstJob?.itemPath ?? null;
|
|
1393
|
+
const serverName = firstJob?.itemName ?? null;
|
|
1394
|
+
const metadataName = parseItemName(firstJob?.metadata, "");
|
|
1395
|
+
const displayName = serverName || metadataName || itemId;
|
|
1396
|
+
const titleAttr = `${itemPath ?? ""}\n${itemId}`.trim();
|
|
1397
|
+
const itemErrorJobs = itemJobs.filter(isRetriableJob);
|
|
1398
|
+
const itemRetryKey = `item:${batchId}:${itemId}`;
|
|
1399
|
+
const isRetryingItem = retryingKeys.has(itemRetryKey);
|
|
1400
|
+
const rowLanguages = itemJobs
|
|
1401
|
+
.map((job) => job.targetLanguage || job.sourceLanguage)
|
|
1402
|
+
.filter((language) => Boolean(language));
|
|
1403
|
+
const normalizedCurrentLanguage = normalizeLanguageCode(currentLanguage);
|
|
1404
|
+
const rowOpenLanguage = rowLanguages.find((language) => normalizeLanguageCode(language) === normalizedCurrentLanguage) ?? rowLanguages[0];
|
|
1405
|
+
const languageChips = (_jsx(InlineLanguageJobChips, { batchId: batchId, batchProvider: batchProvider, jobs: itemJobs, languageMap: languageMap, isExpanded: isItemExpanded, batchStartedAtUtc: batchStartedAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys, singleLine: !stackLanguageChips, onToggle: () => onToggleItem(itemId), onOpenItem: onOpenItem, onAbortJob: onAbortJob, onRetryJobs: onRetryJobs }));
|
|
1406
|
+
const showRetryAction = itemErrorJobs.length > 0;
|
|
1407
|
+
const retryAction = showRetryAction ? (_jsx("div", { className: "flex shrink-0 items-center justify-end", children: _jsx(SimpleIconButton, { onClick: (event) => {
|
|
1408
|
+
event.stopPropagation();
|
|
1409
|
+
if (itemErrorJobs.length === 0)
|
|
1410
|
+
return;
|
|
1411
|
+
void onRetryJobs(batchId, batchProvider, itemErrorJobs, itemRetryKey);
|
|
1412
|
+
}, icon: _jsx(RetryIcon, { className: `h-3.5 w-3.5 ${isRetryingItem ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Retry failed translations for this item", disabled: itemErrorJobs.length === 0 || isRetryingItem }) })) : null;
|
|
1413
|
+
const itemContent = (_jsxs("div", { className: `flex min-w-0 items-start ${stackLanguageChips ? "gap-2" : "gap-2.5"}`, children: [_jsx("span", { className: "mt-1 shrink-0", children: itemAnyInProgress ? (_jsx(LoaderIcon, { className: "h-3 w-3 animate-spin text-highlight-100" })) : itemIsQueued ? (_jsx(QueuedIcon, { className: "h-3 w-3 text-neutral-grey-50", strokeWidth: 1.75, "aria-label": "Queued" })) : (_jsx("span", { className: `block h-1.5 w-1.5 rounded-full ${status.cls}`, "aria-hidden": "true" })) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("span", { className: `block min-w-0 text-[13px] text-neutral-grey-100 ${stackLanguageChips ? "break-words" : "truncate"}`, style: stackLanguageChips
|
|
1414
|
+
? { overflowWrap: "anywhere" }
|
|
1415
|
+
: undefined, title: titleAttr, children: displayName }), itemPath && (_jsx("div", { className: "whitespace-normal break-words font-mono text-[11px] text-neutral-grey-50", style: { overflowWrap: "anywhere" }, title: titleAttr, children: itemPath })), stackLanguageChips && (_jsx("div", { className: "mt-2", children: languageChips })), stackLanguageChips && retryAction && (_jsx("div", { className: "mt-2 flex justify-start", children: retryAction }))] })] }));
|
|
1416
|
+
return (_jsxs("tr", { role: "button", tabIndex: 0, className: `group cursor-pointer transition-colors hover:bg-neutral-grey-5 ${itemIndex > 0 ? "border-t border-border-default/50" : ""}`, onClick: () => {
|
|
1417
|
+
void onOpenItem(itemId, rowOpenLanguage);
|
|
1418
|
+
}, onKeyDown: (e) => {
|
|
1419
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1420
|
+
e.preventDefault();
|
|
1421
|
+
void onOpenItem(itemId, rowOpenLanguage);
|
|
1422
|
+
}
|
|
1423
|
+
}, children: [!stackLanguageChips && (_jsx("td", { "aria-hidden": "true", className: "px-3 py-2 md:px-4" })), _jsx("td", { className: `py-2 align-top ${stackLanguageChips ? "pr-2 pl-2.5" : "pr-4"}`, children: itemContent }), !stackLanguageChips && (_jsx("td", { className: "py-2 pr-2 align-top", children: languageChips })), !stackLanguageChips && (_jsx("td", { className: "py-2 pr-3 align-top md:pr-4", children: retryAction }))] }, itemId));
|
|
1424
|
+
}) })] }) }));
|
|
1117
1425
|
}
|
|
1118
|
-
function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying = false, onRetry, }) {
|
|
1426
|
+
function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isAborting = false, onAbort, isRetrying = false, onRetry, }) {
|
|
1119
1427
|
const [open, setOpen] = React.useState(false);
|
|
1120
1428
|
const jobStatus = job.status;
|
|
1121
1429
|
const jobInProgress = jobStatus === "In Progress";
|
|
1122
1430
|
const jobPending = jobStatus === "Pending";
|
|
1123
1431
|
const jobError = jobStatus === "Error";
|
|
1124
1432
|
const jobAborted = jobStatus === "Aborted";
|
|
1433
|
+
const canAbort = !!job.id && !!job.batchId && !!onAbort && (jobInProgress || jobPending);
|
|
1125
1434
|
const langName = language?.name || job.targetLanguage;
|
|
1126
1435
|
const chipCls = jobError
|
|
1127
1436
|
? "border-feedback-red bg-feedback-red-light text-feedback-red hover:bg-feedback-red-light"
|
|
@@ -1131,16 +1440,16 @@ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying
|
|
|
1131
1440
|
? "border-highlight-100/30 bg-highlight-10 text-highlight-100 hover:brightness-95"
|
|
1132
1441
|
: jobPending
|
|
1133
1442
|
? "border-border-default bg-neutral-grey-5 text-neutral-grey-50 hover:bg-neutral-grey-5"
|
|
1134
|
-
: "border-
|
|
1443
|
+
: "border-feedback-green bg-feedback-green-light text-feedback-green hover:border-feedback-green hover:brightness-95";
|
|
1135
1444
|
const statusBadgeCls = jobError
|
|
1136
|
-
? "bg-feedback-red-light text-feedback-red
|
|
1445
|
+
? "bg-feedback-red-light text-feedback-red"
|
|
1137
1446
|
: jobAborted
|
|
1138
|
-
? "bg-feedback-orange-light text-feedback-orange
|
|
1447
|
+
? "bg-feedback-orange-light text-feedback-orange"
|
|
1139
1448
|
: jobInProgress
|
|
1140
|
-
? "bg-highlight-10 text-highlight-100
|
|
1449
|
+
? "bg-highlight-10 text-highlight-100"
|
|
1141
1450
|
: jobPending
|
|
1142
|
-
? "bg-neutral-grey-5 text-neutral-grey-50
|
|
1143
|
-
: "bg-feedback-green-light text-feedback-green
|
|
1451
|
+
? "bg-neutral-grey-5 text-neutral-grey-50"
|
|
1452
|
+
: "bg-feedback-green-light text-feedback-green";
|
|
1144
1453
|
const timestamp = job.timestamp ? new Date(job.timestamp) : null;
|
|
1145
1454
|
const timestampValid = timestamp && !isNaN(timestamp.getTime());
|
|
1146
1455
|
const claimedAt = job.claimedAtUtc ? new Date(job.claimedAtUtc) : null;
|
|
@@ -1198,15 +1507,19 @@ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying
|
|
|
1198
1507
|
const emptyFieldText = statistics && statistics.emptyFieldCount > 0
|
|
1199
1508
|
? `${formatCount(statistics.emptyFieldCount)} empty`
|
|
1200
1509
|
: null;
|
|
1201
|
-
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: `inline-flex items-center gap-1.5 rounded
|
|
1510
|
+
return (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: `group inline-flex items-center gap-1.5 rounded border badge-pad-sm text-[12px] transition-colors cursor-pointer ${chipCls}`, children: [language?.icon ? (_jsx("img", { src: language.icon, alt: langName, className: "h-3.5 shrink-0" })) : null, _jsx("span", { className: "font-mono tracking-tight", children: job.targetLanguage }), jobInProgress && (_jsx(LoaderIcon, { className: "h-3 w-3 shrink-0 animate-spin" })), jobPending && (_jsx(QueuedIcon, { className: "h-3 w-3 shrink-0", strokeWidth: 1.75, "aria-label": "Queued" })), jobError && _jsx(AlertCircleIcon, { className: "h-3 w-3 shrink-0" }), !jobInProgress && !jobError && !jobPending && !jobAborted && (_jsx(CheckIcon, { className: "h-3 w-3 shrink-0 text-feedback-green" }))] }) }), _jsxs(PopoverContent, { align: "start", sideOffset: 6, className: "w-[22rem] max-w-[calc(100vw-2rem)] p-0", children: [_jsxs("div", { className: "flex items-center gap-2 border-b border-border-default px-3 py-2", children: [language?.icon ? (_jsx("img", { src: language.icon, alt: langName, className: "h-4 shrink-0" })) : null, _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("div", { className: "text-[13px] font-medium text-neutral-grey-100 truncate", children: langName }), _jsxs("div", { className: "text-[11px] text-neutral-grey-50 font-mono truncate", children: [job.targetLanguage, job.sourceLanguage ? ` ← ${job.sourceLanguage}` : ""] })] }), _jsxs(Badge, { size: "sm", className: statusBadgeCls, children: [jobInProgress && (_jsx(LoaderIcon, { className: "h-2.5 w-2.5 animate-spin" })), jobStatus || "Unknown"] })] }), _jsxs("dl", { className: "divide-y divide-border-default/60", children: [claimedAtValid && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Claimed" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: formatDateTime(claimedAt) })] })), timestampValid && (jobInProgress || jobPending) && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Updated" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: formatDateTime(timestamp) })] })), durationMs != null && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: jobInProgress || jobPending ? "Running" : "Duration" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", title: durationApproximated
|
|
1202
1511
|
? "Approximate — measured from batch start"
|
|
1203
|
-
: undefined, children: [durationApproximated ? "~" : "", formatDuration(durationMs)] })] })), statistics && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Fields" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: [fieldText, emptyFieldText ? (_jsxs("span", { className: "ml-1.5 text-neutral-grey-50", children: ["(", emptyFieldText, ")"] })) : null] })] })), statistics && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Text" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: [formatCount(statistics.sourceCharacterCount), " chars"] })] })), job.totalCost != null && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Cost" }), _jsx("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: formatUsdCost(job.totalCost) })] })), lastHeartbeatValid && (jobInProgress || jobPending) && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Heartbeat" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: lastHeartbeat
|
|
1512
|
+
: undefined, children: [durationApproximated ? "~" : "", formatDuration(durationMs)] })] })), statistics && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Fields" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: [fieldText, emptyFieldText ? (_jsxs("span", { className: "ml-1.5 text-neutral-grey-50", children: ["(", emptyFieldText, ")"] })) : null] })] })), statistics && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Text" }), _jsxs("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: [formatCount(statistics.sourceCharacterCount), " chars"] })] })), job.totalCost != null && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Cost" }), _jsx("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: formatUsdCost(job.totalCost) })] })), lastHeartbeatValid && (jobInProgress || jobPending) && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Heartbeat" }), _jsx("dd", { className: "text-[12px] text-neutral-grey-100", children: formatDateTime(lastHeartbeat) })] })), attemptCount > 1 && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Attempts" }), _jsx("dd", { className: "text-[12px] tabular-nums text-neutral-grey-100", children: attemptCount })] })), job.hash && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Hash" }), _jsx("dd", { className: "text-[11px] font-mono text-neutral-grey-100 break-all", children: job.hash })] })), job.message &&
|
|
1204
1513
|
(jobError || jobAborted ? (
|
|
1205
1514
|
// Error/abort messages can be long and multi-line (e.g. an external
|
|
1206
1515
|
// ITranslationService stack trace). Render them in a bounded,
|
|
1207
1516
|
// scrollable monospace block so the popover stays a sensible size and
|
|
1208
1517
|
// the original formatting is preserved.
|
|
1209
|
-
_jsxs("div", { className: "flex flex-col gap-1 px-3 py-2", children: [_jsx("dt", { className: "text-[10px] tracking-wide text-neutral-grey-50", children: jobError ? "Error" : "Aborted" }), _jsx("dd", { className: `max-h-40 overflow-auto whitespace-pre-wrap break-words rounded-sm bg-neutral-grey-5 p-2 font-mono text-[11px] leading-snug ${jobError ? "text-feedback-red" : "text-feedback-orange"}`, children: job.message })] })) : (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Progress" }), _jsx("dd", { className: "text-[12px] break-words text-neutral-grey-100", children: job.message })] }))), job.batchId && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Batch" }), _jsx("dd", { className: "text-[11px] font-mono text-neutral-grey-50 break-all", children: job.batchId })] }))] }), _jsxs("div", { className: "flex justify-end gap-2 border-t border-border-default px-3 py-2", children: [
|
|
1518
|
+
_jsxs("div", { className: "flex flex-col gap-1 px-3 py-2", children: [_jsx("dt", { className: "text-[10px] tracking-wide text-neutral-grey-50", children: jobError ? "Error" : "Aborted" }), _jsx("dd", { className: `max-h-40 overflow-auto whitespace-pre-wrap break-words rounded-sm bg-neutral-grey-5 p-2 font-mono text-[11px] leading-snug ${jobError ? "text-feedback-red" : "text-feedback-orange"}`, children: job.message })] })) : (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Progress" }), _jsx("dd", { className: "text-[12px] break-words text-neutral-grey-100", children: job.message })] }))), job.batchId && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Batch" }), _jsx("dd", { className: "text-[11px] font-mono text-neutral-grey-50 break-all", children: job.batchId })] }))] }), _jsxs("div", { className: "flex justify-end gap-2 border-t border-border-default px-3 py-2", children: [canAbort && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isAborting, onClick: (e) => {
|
|
1519
|
+
e.stopPropagation();
|
|
1520
|
+
void onAbort?.();
|
|
1521
|
+
setOpen(false);
|
|
1522
|
+
}, className: "gap-1.5 text-feedback-orange hover:text-feedback-orange", children: [_jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}` }), "Abort"] })), jobError && onRetry && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isRetrying, onClick: (e) => {
|
|
1210
1523
|
e.stopPropagation();
|
|
1211
1524
|
void onRetry();
|
|
1212
1525
|
setOpen(false);
|