@parhelia/localization 0.1.12910 → 0.1.12912
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 +541 -234
- 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, DeleteIcon, } from "@parhelia/core";
|
|
4
|
+
import { Button, Badge, Select, Input, UserPicker, Popover, PopoverContent, PopoverTrigger, Tooltip, TooltipContent, TooltipTrigger, ContentTree, PageHeader, fetchItemStubs, searchUsers as searchBackendUsers, useEditContext, getLanguages, SimpleIconButton, DeleteIcon, formatDateTime, } from "@parhelia/core";
|
|
5
5
|
import { listBatches, getTranslationProviders, listBatchTranslationJobs, retryBatchTranslation, abortBatch, deleteBatch, } 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,14 +177,11 @@ 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({});
|
|
@@ -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)
|
|
@@ -549,7 +605,7 @@ export function TranslationBatches() {
|
|
|
549
605
|
]);
|
|
550
606
|
const handleDeleteBatch = useCallback((batchId) => {
|
|
551
607
|
editContext?.confirm({
|
|
552
|
-
header: "Delete Translation History",
|
|
608
|
+
header: "Delete Translation Batch from History",
|
|
553
609
|
message: "Delete this translation batch from history? This removes only translation job records; it does not delete Sitecore items or translated content.",
|
|
554
610
|
acceptLabel: "Delete",
|
|
555
611
|
showCancel: true,
|
|
@@ -652,67 +708,310 @@ export function TranslationBatches() {
|
|
|
652
708
|
const openItemInEditor = useCallback(async (itemId, language) => {
|
|
653
709
|
const normalized = (itemId ?? "").toString().toLowerCase();
|
|
654
710
|
editContext?.setShowAgentsWorkspaceEditor(true);
|
|
655
|
-
await editContext?.loadItem({ id: normalized, language, version: 0 });
|
|
711
|
+
await editContext?.loadItem(language ? { id: normalized, language, version: 0 } : normalized);
|
|
656
712
|
}, [editContext]);
|
|
657
|
-
|
|
658
|
-
|
|
713
|
+
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
|
|
714
|
+
? undefined
|
|
715
|
+
: "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: (() => {
|
|
716
|
+
const hasActiveFilters = filters.dateRange !== "last30days" ||
|
|
717
|
+
filters.status !== "all" ||
|
|
718
|
+
filters.user !== currentUserName ||
|
|
719
|
+
filters.provider !== "all" ||
|
|
720
|
+
filters.targetLanguage !== "all" ||
|
|
721
|
+
filters.itemIdOrPath.trim() !== "" ||
|
|
722
|
+
filters.itemIncludeSubitems;
|
|
723
|
+
return hasActiveFilters
|
|
724
|
+
? "Try adjusting your filters or start a translation to see it appear here"
|
|
725
|
+
: "Start a translation to see it appear here";
|
|
726
|
+
})() })] }) })) : (_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) => {
|
|
727
|
+
const batchDate = new Date(b.timestamp);
|
|
728
|
+
const formattedBatchDate = formatDateTime(batchDate);
|
|
729
|
+
const info = b.info;
|
|
730
|
+
const totalJobs = info?.expectedJobs ?? 0;
|
|
731
|
+
const itemsCount = typeof info?.itemsCount === "number"
|
|
732
|
+
? info.itemsCount
|
|
733
|
+
: undefined;
|
|
734
|
+
const languagesCsv = info?.languages;
|
|
735
|
+
const languages = languagesCsv
|
|
736
|
+
? languagesCsv.split(",")
|
|
737
|
+
: [];
|
|
738
|
+
const completedJobs = info?.completedJobs ?? 0;
|
|
739
|
+
const errorJobs = info?.errorJobs ?? 0;
|
|
740
|
+
const anyError = errorJobs > 0;
|
|
741
|
+
const anyInProgress = isActiveBatchStatus(info?.status);
|
|
742
|
+
const anyAborted = info?.status === "Aborted";
|
|
743
|
+
const progressPct = totalJobs > 0
|
|
744
|
+
? Math.min(100, Math.round((completedJobs / totalJobs) * 100))
|
|
745
|
+
: 0;
|
|
746
|
+
const progressTone = anyError
|
|
747
|
+
? "error"
|
|
748
|
+
: anyAborted
|
|
749
|
+
? "aborted"
|
|
750
|
+
: progressPct >= 100
|
|
751
|
+
? "complete"
|
|
752
|
+
: "active";
|
|
753
|
+
const metaParts = [];
|
|
754
|
+
metaParts.push(_jsxs("span", { className: "text-neutral-grey-100", children: [totalJobs, " translation", totalJobs !== 1 ? "s" : ""] }, "trans"));
|
|
755
|
+
if (typeof itemsCount === "number") {
|
|
756
|
+
metaParts.push(_jsxs("span", { children: [itemsCount, " item", itemsCount !== 1 ? "s" : ""] }, "items"));
|
|
757
|
+
}
|
|
758
|
+
if (languages.length > 0) {
|
|
759
|
+
metaParts.push(_jsxs("span", { title: languages.join(", "), children: [languages.length, " language", languages.length !== 1 ? "s" : ""] }, "langs"));
|
|
760
|
+
}
|
|
761
|
+
if (info?.provider) {
|
|
762
|
+
metaParts.push(_jsx("span", { children: info.provider }, "prov"));
|
|
763
|
+
}
|
|
764
|
+
if (info?.totalCost != null) {
|
|
765
|
+
metaParts.push(_jsxs("span", { children: ["Cost:", " ", _jsx("span", { className: "font-medium text-neutral-grey-100", children: formatUsdCost(info.totalCost) })] }, "cost"));
|
|
766
|
+
}
|
|
767
|
+
const ownerPart = info?.initiatedByUser ? (_jsx("span", { title: info.initiatedByUser, children: userDisplayNames[info.initiatedByUser] ||
|
|
768
|
+
info.initiatedByUser }, "user")) : null;
|
|
769
|
+
const datePart = info?.lastUpdatedUtc ? (_jsx("span", { children: formatDateTime(new Date(info.lastUpdatedUtc)) }, "date")) : null;
|
|
770
|
+
const desktopMetaParts = [...metaParts];
|
|
771
|
+
if (!isCompactBatches) {
|
|
772
|
+
if (ownerPart)
|
|
773
|
+
desktopMetaParts.push(ownerPart);
|
|
774
|
+
if (datePart)
|
|
775
|
+
desktopMetaParts.push(datePart);
|
|
776
|
+
}
|
|
777
|
+
const mobileFooterMetaParts = [];
|
|
778
|
+
if (isCompactBatches) {
|
|
779
|
+
if (datePart)
|
|
780
|
+
mobileFooterMetaParts.push(datePart);
|
|
781
|
+
if (ownerPart)
|
|
782
|
+
mobileFooterMetaParts.push(ownerPart);
|
|
783
|
+
}
|
|
784
|
+
const batchDisplayName = (() => {
|
|
785
|
+
let parsedName;
|
|
786
|
+
if (info?.metadata) {
|
|
787
|
+
try {
|
|
788
|
+
const m = typeof info.metadata === "string"
|
|
789
|
+
? JSON.parse(info.metadata)
|
|
790
|
+
: info.metadata;
|
|
791
|
+
const candidate = m?.name;
|
|
792
|
+
if (typeof candidate === "string" &&
|
|
793
|
+
candidate.trim()) {
|
|
794
|
+
parsedName = candidate.trim();
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
// Metadata isn't JSON - fall through to timestamp.
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return (parsedName ??
|
|
802
|
+
`Translation Batch · ${formattedBatchDate}`);
|
|
803
|
+
})();
|
|
804
|
+
const isExpanded = expandedBatchId === b.id;
|
|
805
|
+
const jobs = batchJobs[b.id];
|
|
806
|
+
const isLoadingThis = loadingJobs.has(b.id);
|
|
807
|
+
const canRetry = anyError;
|
|
808
|
+
const canAbort = isActiveBatchStatus(info?.status);
|
|
809
|
+
const canDelete = isTerminalBatchStatus(info?.status);
|
|
810
|
+
const isRetryingBatch = retryingKeys.has(`batch:${b.id}`);
|
|
811
|
+
const isAborting = abortingBatchIds.has(b.id);
|
|
812
|
+
const isDeleting = deletingBatchIds.has(b.id);
|
|
813
|
+
const hasBatchActions = canRetry || canAbort || canDelete;
|
|
814
|
+
// Stable test-id status used by Playwright assertions.
|
|
815
|
+
// Matches the visual status dot/spinner logic above.
|
|
816
|
+
const testStatus = anyError
|
|
817
|
+
? "error"
|
|
818
|
+
: anyInProgress
|
|
819
|
+
? "in-progress"
|
|
820
|
+
: anyAborted
|
|
821
|
+
? "aborted"
|
|
822
|
+
: "completed";
|
|
823
|
+
const batchActions = (_jsxs(_Fragment, { children: [canRetry && (_jsx(SimpleIconButton, { onClick: (event) => {
|
|
824
|
+
event.stopPropagation();
|
|
825
|
+
if (!canRetry)
|
|
826
|
+
return;
|
|
827
|
+
void handleRetryBatch(info);
|
|
828
|
+
}, icon: _jsx(RetryIcon, { className: `h-3.5 w-3.5 ${isRetryingBatch ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Retry failed translations", disabled: !canRetry ||
|
|
829
|
+
isRetryingBatch ||
|
|
830
|
+
isLoadingThis ||
|
|
831
|
+
isAborting ||
|
|
832
|
+
isDeleting })), canAbort && (_jsx(SimpleIconButton, { onClick: (event) => {
|
|
833
|
+
event.stopPropagation();
|
|
834
|
+
if (!canAbort)
|
|
835
|
+
return;
|
|
836
|
+
handleAbortBatch(b.id);
|
|
837
|
+
}, icon: _jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Abort translation batch", disabled: !canAbort || isAborting || isDeleting })), canDelete && (_jsx(SimpleIconButton, { onClick: (event) => {
|
|
838
|
+
event.stopPropagation();
|
|
839
|
+
if (!canDelete)
|
|
840
|
+
return;
|
|
841
|
+
handleDeleteBatch(b.id);
|
|
842
|
+
}, icon: _jsx(DeleteIcon, { size: "md" }), label: "Delete translation batch from history", disabled: !canDelete || isDeleting || isAborting }))] }));
|
|
843
|
+
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) => {
|
|
844
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
845
|
+
e.preventDefault();
|
|
846
|
+
toggleBatch(b.id);
|
|
847
|
+
}
|
|
848
|
+
}, 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
|
|
849
|
+
? "flex-col gap-2"
|
|
850
|
+
: "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
|
|
851
|
+
? "whitespace-normal break-words leading-5"
|
|
852
|
+
: "truncate"}`, style: isCompactBatches
|
|
853
|
+
? { overflowWrap: "anywhere" }
|
|
854
|
+
: 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, retryingKeys: retryingKeys, isMobile: isMobile, onToggleItem: toggleItem, onOpenItem: openItemInEditor, onRetryJobs: handleRetryJobs })] }))] }, b.id));
|
|
855
|
+
}) })] }, 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"] })) }) }))] })) }) })] }));
|
|
856
|
+
}
|
|
857
|
+
export const TRANSLATION_BATCH_FILTERS_SIDEBAR_ID = "translation-batch-filters";
|
|
858
|
+
export function TranslationBatchFiltersSidebar() {
|
|
859
|
+
const editContext = useEditContext();
|
|
860
|
+
const { filters, itemIdOrPathInput } = useTranslationBatchFilterSnapshot();
|
|
861
|
+
const [providers, setProviders] = useState([]);
|
|
862
|
+
const [sitecoreLanguages, setSitecoreLanguages] = useState([]);
|
|
863
|
+
const [itemPickerOpen, setItemPickerOpen] = useState(false);
|
|
864
|
+
const currentUserName = editContext?.user?.name || "all";
|
|
865
|
+
const isLimitedPreviewUser = editContext?.user?.isLimitedPreviewUser ?? false;
|
|
866
|
+
useEffect(() => {
|
|
867
|
+
ensureDefaultTranslationBatchUser(currentUserName);
|
|
868
|
+
}, [currentUserName]);
|
|
869
|
+
useEffect(() => {
|
|
870
|
+
if (isLimitedPreviewUser &&
|
|
871
|
+
currentUserName !== "all" &&
|
|
872
|
+
filters.user !== currentUserName) {
|
|
873
|
+
setTranslationBatchFilters((prev) => ({
|
|
874
|
+
...prev,
|
|
875
|
+
user: currentUserName,
|
|
876
|
+
}));
|
|
877
|
+
}
|
|
878
|
+
}, [isLimitedPreviewUser, currentUserName, filters.user]);
|
|
879
|
+
const searchTranslationUsers = useCallback((query, limit) => searchBackendUsers(query, limit), []);
|
|
880
|
+
const debouncedSetItemFilter = useDebouncedCallback((value) => setTranslationBatchFilters((prev) => ({
|
|
881
|
+
...prev,
|
|
882
|
+
itemIdOrPath: value,
|
|
883
|
+
})), 400);
|
|
884
|
+
useEffect(() => {
|
|
885
|
+
const loadProviders = async () => {
|
|
886
|
+
try {
|
|
887
|
+
const res = await getTranslationProviders();
|
|
888
|
+
const maybeProviders = res?.data ?? res;
|
|
889
|
+
setProviders(Array.isArray(maybeProviders)
|
|
890
|
+
? maybeProviders
|
|
891
|
+
: []);
|
|
892
|
+
}
|
|
893
|
+
catch (error) {
|
|
894
|
+
console.error("Failed to load translation providers:", error);
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
loadProviders();
|
|
898
|
+
}, []);
|
|
899
|
+
useEffect(() => {
|
|
900
|
+
const loadLanguages = async () => {
|
|
901
|
+
try {
|
|
902
|
+
const res = await getLanguages();
|
|
903
|
+
const langs = (res?.data ?? res ?? []);
|
|
904
|
+
setSitecoreLanguages(Array.isArray(langs) ? langs : []);
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
console.error("Failed to load Sitecore languages:", error);
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
loadLanguages();
|
|
911
|
+
}, []);
|
|
912
|
+
const languageOptions = useMemo(() => sitecoreLanguages
|
|
913
|
+
.map((language) => {
|
|
914
|
+
const code = language.languageCode || language.name;
|
|
915
|
+
return code ? { ...language, languageCode: code } : null;
|
|
916
|
+
})
|
|
917
|
+
.filter((language) => language !== null)
|
|
918
|
+
.sort((a, b) => a.languageCode.localeCompare(b.languageCode)), [sitecoreLanguages]);
|
|
919
|
+
const currentItem = editContext?.item;
|
|
920
|
+
const multiItemEnabled = editContext?.configuration?.localization?.multiItem !== false;
|
|
921
|
+
const canStartTranslation = !!editContext && (multiItemEnabled || !!currentItem);
|
|
922
|
+
const handleStartTranslation = useCallback(() => {
|
|
923
|
+
if (!editContext)
|
|
924
|
+
return;
|
|
925
|
+
void openLocalizeItemDialog({
|
|
926
|
+
editContext,
|
|
927
|
+
items: currentItem ? [currentItem] : [],
|
|
928
|
+
});
|
|
929
|
+
}, [currentItem, editContext]);
|
|
659
930
|
const hasActiveFilters = filters.dateRange !== "last30days" ||
|
|
660
931
|
filters.status !== "all" ||
|
|
661
932
|
filters.user !== currentUserName ||
|
|
662
933
|
filters.provider !== "all" ||
|
|
663
934
|
filters.targetLanguage !== "all" ||
|
|
664
|
-
filters.itemIdOrPath.trim() !== ""
|
|
665
|
-
|
|
666
|
-
|
|
935
|
+
filters.itemIdOrPath.trim() !== "" ||
|
|
936
|
+
filters.itemIncludeSubitems;
|
|
937
|
+
const resetFilters = useCallback(() => {
|
|
938
|
+
debouncedSetItemFilter.cancel();
|
|
939
|
+
setTranslationBatchItemInput("");
|
|
940
|
+
setTranslationBatchFilters({
|
|
941
|
+
...DEFAULT_FILTERS,
|
|
942
|
+
user: currentUserName,
|
|
943
|
+
});
|
|
944
|
+
}, [currentUserName, debouncedSetItemFilter]);
|
|
945
|
+
const filterLabelClass = "text-[12px] font-medium text-neutral-grey-50";
|
|
946
|
+
const filterFieldClass = "flex flex-col gap-1";
|
|
947
|
+
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
|
|
948
|
+
? "Start new translation"
|
|
949
|
+
: "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
950
|
...prev,
|
|
668
951
|
dateRange: value,
|
|
669
|
-
})), placeholder: "Select date range" })] }), _jsxs("div", { className:
|
|
952
|
+
})), 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
953
|
...prev,
|
|
671
954
|
status: value,
|
|
672
|
-
})), placeholder: "Select status" })] }), _jsxs("div", { className:
|
|
955
|
+
})), placeholder: "Select status" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "User" }), _jsx(UserPicker, { value: isLimitedPreviewUser
|
|
673
956
|
? currentUserName
|
|
674
957
|
: filters.user === "all"
|
|
675
958
|
? null
|
|
676
959
|
: filters.user, displayValue: isLimitedPreviewUser || filters.user === currentUserName
|
|
677
960
|
? `${editContext?.user?.fullName?.trim() || currentUserName} (You)`
|
|
678
|
-
: undefined,
|
|
961
|
+
: undefined, displayAvatarName: isLimitedPreviewUser || filters.user === currentUserName
|
|
962
|
+
? editContext?.user?.fullName?.trim() || currentUserName
|
|
963
|
+
: undefined, displayAvatarUrl: isLimitedPreviewUser || filters.user === currentUserName
|
|
964
|
+
? editContext?.user?.avatarUrl
|
|
965
|
+
: undefined, onChange: (user) => setTranslationBatchFilters((prev) => ({
|
|
679
966
|
...prev,
|
|
680
967
|
user: user?.userName ?? "all",
|
|
681
|
-
})), disabled: isLimitedPreviewUser, placeholder: "All Users", buttonClassName: "h-[
|
|
968
|
+
})), disabled: isLimitedPreviewUser, placeholder: "All Users", buttonClassName: "h-auto min-h-[34px] w-full justify-start rounded-md border bg-white py-2 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
969
|
{ value: "all", label: "All Providers" },
|
|
683
970
|
...providers.map((p) => ({
|
|
684
971
|
value: p.name,
|
|
685
972
|
label: p.displayName || p.name,
|
|
686
973
|
})),
|
|
687
|
-
], value: filters.provider, onValueChange: (value) =>
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
974
|
+
], value: filters.provider, onValueChange: (value) => setTranslationBatchFilters((prev) => ({
|
|
975
|
+
...prev,
|
|
976
|
+
provider: value,
|
|
977
|
+
})), placeholder: "Select provider" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Target Language" }), _jsx(Select, { className: "w-full bg-white", options: [
|
|
978
|
+
{
|
|
979
|
+
value: "all",
|
|
980
|
+
label: "All Languages",
|
|
981
|
+
},
|
|
982
|
+
...languageOptions.map((language) => ({
|
|
983
|
+
value: language.languageCode,
|
|
984
|
+
label: language.languageCode,
|
|
985
|
+
icon: renderTargetLanguageIcon(language),
|
|
692
986
|
})),
|
|
693
|
-
], value: filters.targetLanguage, onValueChange: (value) =>
|
|
987
|
+
], value: filters.targetLanguage, onValueChange: (value) => setTranslationBatchFilters((prev) => ({
|
|
988
|
+
...prev,
|
|
989
|
+
targetLanguage: value,
|
|
990
|
+
})), 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
991
|
...prev,
|
|
695
992
|
itemIncludeSubitems: e.target.checked,
|
|
696
|
-
})), className: "h-3 w-3 accent-highlight-100
|
|
993
|
+
})), className: "h-3 w-3 cursor-pointer accent-highlight-100" }), "Include subitems"] })] }), _jsxs("div", { className: "relative", children: [_jsx(Input, { value: itemIdOrPathInput, onChange: (e) => {
|
|
697
994
|
const next = e.target.value;
|
|
698
|
-
|
|
995
|
+
setTranslationBatchItemInput(next);
|
|
699
996
|
debouncedSetItemFilter(next);
|
|
700
|
-
}, placeholder: "GUID or /sitecore/content
|
|
701
|
-
|
|
997
|
+
}, 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: () => {
|
|
998
|
+
setTranslationBatchItemInput("");
|
|
702
999
|
debouncedSetItemFilter.cancel();
|
|
703
|
-
|
|
1000
|
+
setTranslationBatchFilters((prev) => ({
|
|
704
1001
|
...prev,
|
|
705
1002
|
itemIdOrPath: "",
|
|
706
1003
|
itemIncludeSubitems: false,
|
|
707
1004
|
}));
|
|
708
|
-
}, className: "text-neutral-grey-50 hover:text-neutral-grey-100
|
|
1005
|
+
}, 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: [
|
|
1006
|
+
"0de95ae4-41ab-4d01-9eb0-67441b7c2450",
|
|
1007
|
+
], onSelectionChange: (items) => {
|
|
709
1008
|
const picked = items?.[0];
|
|
710
1009
|
if (!picked)
|
|
711
1010
|
return;
|
|
712
1011
|
const applyValue = (value) => {
|
|
713
|
-
|
|
1012
|
+
setTranslationBatchItemInput(value);
|
|
714
1013
|
debouncedSetItemFilter.cancel();
|
|
715
|
-
|
|
1014
|
+
setTranslationBatchFilters((prev) => ({
|
|
716
1015
|
...prev,
|
|
717
1016
|
itemIdOrPath: value,
|
|
718
1017
|
}));
|
|
@@ -722,134 +1021,19 @@ export function TranslationBatches() {
|
|
|
722
1021
|
applyValue(picked.path);
|
|
723
1022
|
return;
|
|
724
1023
|
}
|
|
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
1024
|
fetchItemStubs([
|
|
728
|
-
{
|
|
1025
|
+
{
|
|
1026
|
+
id: picked.id,
|
|
1027
|
+
language: "en",
|
|
1028
|
+
version: 1,
|
|
1029
|
+
},
|
|
729
1030
|
])
|
|
730
1031
|
.then((stubs) => {
|
|
731
1032
|
const stubPath = stubs?.[0]?.path;
|
|
732
1033
|
applyValue(stubPath || picked.id);
|
|
733
1034
|
})
|
|
734
1035
|
.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"] })) }) }))] })) })] }));
|
|
1036
|
+
} }) })] })] })] })] })] }), _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
1037
|
}
|
|
854
1038
|
function isLikelyGuid(value) {
|
|
855
1039
|
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 +1116,45 @@ function formatUsdCost(value) {
|
|
|
932
1116
|
maximumFractionDigits: value > 0 && value < 0.01 ? 6 : 2,
|
|
933
1117
|
}).format(value);
|
|
934
1118
|
}
|
|
1119
|
+
function BatchProgressIndicator({ value, completedJobs, totalJobs, errorJobs, tone, testId, variant = "circle", }) {
|
|
1120
|
+
const percent = Math.max(0, Math.min(100, Math.round(value)));
|
|
1121
|
+
const radius = 17;
|
|
1122
|
+
const circumference = 2 * Math.PI * radius;
|
|
1123
|
+
const dashOffset = circumference - (percent / 100) * circumference;
|
|
1124
|
+
const hasErrors = errorJobs > 0;
|
|
1125
|
+
const toneClass = tone === "complete"
|
|
1126
|
+
? "text-feedback-green"
|
|
1127
|
+
: tone === "error"
|
|
1128
|
+
? "text-feedback-red"
|
|
1129
|
+
: tone === "aborted"
|
|
1130
|
+
? "text-feedback-orange"
|
|
1131
|
+
: "text-neutral-grey-50";
|
|
1132
|
+
const toneColor = tone === "complete"
|
|
1133
|
+
? "var(--color-feedback-green)"
|
|
1134
|
+
: tone === "error"
|
|
1135
|
+
? "var(--color-feedback-red)"
|
|
1136
|
+
: tone === "aborted"
|
|
1137
|
+
? "var(--color-feedback-orange)"
|
|
1138
|
+
: "var(--color-neutral-grey-50)";
|
|
1139
|
+
const trackColor = tone === "complete"
|
|
1140
|
+
? "var(--color-feedback-green-light)"
|
|
1141
|
+
: tone === "error"
|
|
1142
|
+
? "var(--color-feedback-red-light)"
|
|
1143
|
+
: tone === "aborted"
|
|
1144
|
+
? "var(--color-feedback-orange-light)"
|
|
1145
|
+
: "var(--color-neutral-grey-15)";
|
|
1146
|
+
const label = `${percent}% complete (${completedJobs}/${totalJobs} translations${hasErrors ? `, ${errorJobs} failed` : ""})`;
|
|
1147
|
+
if (variant === "bar") {
|
|
1148
|
+
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: {
|
|
1149
|
+
width: `${percent}%`,
|
|
1150
|
+
backgroundColor: toneColor,
|
|
1151
|
+
} }) }), _jsxs("span", { className: `shrink-0 text-[11px] leading-none font-medium tabular-nums ${toneClass}`, children: [percent, "%"] })] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: label })] }));
|
|
1152
|
+
}
|
|
1153
|
+
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: {
|
|
1154
|
+
strokeDasharray: circumference,
|
|
1155
|
+
strokeDashoffset: dashOffset,
|
|
1156
|
+
} })] }), _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 })] }));
|
|
1157
|
+
}
|
|
935
1158
|
function itemStatusDot(jobs) {
|
|
936
1159
|
const hasError = jobs.some((j) => j.status === "Error");
|
|
937
1160
|
const hasInProgress = jobs.some((j) => j.status === "In Progress");
|
|
@@ -947,6 +1170,30 @@ function itemStatusDot(jobs) {
|
|
|
947
1170
|
return { cls: "bg-feedback-orange", label: "Aborted" };
|
|
948
1171
|
return { cls: "bg-feedback-green/70", label: "Completed" };
|
|
949
1172
|
}
|
|
1173
|
+
function InlineLanguageJobChips({ batchId, batchProvider, jobs, languageMap, isExpanded, batchStartedAtUtc, retryingKeys, singleLine = false, onToggle, onOpenItem, onRetryJobs, }) {
|
|
1174
|
+
if (jobs.length === 0)
|
|
1175
|
+
return null;
|
|
1176
|
+
const handleCellClick = (event) => {
|
|
1177
|
+
event.stopPropagation();
|
|
1178
|
+
onToggle();
|
|
1179
|
+
};
|
|
1180
|
+
const handleCellKeyDown = (event) => {
|
|
1181
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
1182
|
+
event.preventDefault();
|
|
1183
|
+
event.stopPropagation();
|
|
1184
|
+
onToggle();
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
return (_jsx("div", { role: "button", tabIndex: 0, className: `flex min-w-0 cursor-pointer items-start justify-start gap-1.5 ${singleLine
|
|
1188
|
+
? "flex-nowrap"
|
|
1189
|
+
: isExpanded
|
|
1190
|
+
? "flex-wrap"
|
|
1191
|
+
: "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) => {
|
|
1192
|
+
const lang = languageMap.get((job.targetLanguage || "").toLowerCase());
|
|
1193
|
+
const retryKey = `job:${job.id ?? `${job.itemId}:${job.sourceLanguage}:${job.targetLanguage}`}`;
|
|
1194
|
+
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), isRetrying: retryingKeys.has(retryKey), onRetry: () => onRetryJobs(batchId, batchProvider, [job], retryKey) }) }, `${job.id ?? idx}-${job.targetLanguage}`));
|
|
1195
|
+
}) }));
|
|
1196
|
+
}
|
|
950
1197
|
function CustomPromptPanel({ prompts }) {
|
|
951
1198
|
const [openProvider, setOpenProvider] = React.useState(null);
|
|
952
1199
|
const [copiedProvider, setCopiedProvider] = React.useState(null);
|
|
@@ -971,7 +1218,7 @@ function CustomPromptPanel({ prompts }) {
|
|
|
971
1218
|
toast.error("Failed to copy prompt to clipboard");
|
|
972
1219
|
}
|
|
973
1220
|
};
|
|
974
|
-
return (_jsx("div", { className: "
|
|
1221
|
+
return (_jsx("div", { className: "border-b border-border-default/50", children: prompts.map((p) => {
|
|
975
1222
|
const isOpen = openProvider === p.provider;
|
|
976
1223
|
const justCopied = copiedProvider === p.provider;
|
|
977
1224
|
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 +1229,10 @@ function CustomPromptPanel({ prompts }) {
|
|
|
982
1229
|
}, 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
1230
|
event.stopPropagation();
|
|
984
1231
|
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"
|
|
1232
|
+
}, 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
1233
|
}) }));
|
|
987
1234
|
}
|
|
988
|
-
function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems, languages, itemFilter, itemIncludeSubitems, statusFilter, batchIsTerminal, batchStartedAtUtc, retryingKeys, onToggleItem, onOpenItem, onRetryJobs, }) {
|
|
1235
|
+
function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems, languages, currentLanguage, itemFilter, itemIncludeSubitems, statusFilter, batchIsTerminal, batchStartedAtUtc, retryingKeys, isMobile = false, onToggleItem, onOpenItem, onRetryJobs, }) {
|
|
989
1236
|
const languageMap = React.useMemo(() => {
|
|
990
1237
|
const map = new Map();
|
|
991
1238
|
for (const l of languages) {
|
|
@@ -1066,6 +1313,57 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
|
|
|
1066
1313
|
return [];
|
|
1067
1314
|
return filteredJobs.filter((j) => matchingItemIds.has((j.itemId || "").toString().toLowerCase()));
|
|
1068
1315
|
}, [filteredJobs, statusFilter]);
|
|
1316
|
+
const [stackLanguageChips, setStackLanguageChips] = React.useState(isMobile);
|
|
1317
|
+
const itemTableContainerRef = React.useRef(null);
|
|
1318
|
+
const itemTableRef = React.useRef(null);
|
|
1319
|
+
const normalTableMinWidthRef = React.useRef(null);
|
|
1320
|
+
React.useEffect(() => {
|
|
1321
|
+
const container = itemTableContainerRef.current;
|
|
1322
|
+
const table = itemTableRef.current;
|
|
1323
|
+
if (!container || !table)
|
|
1324
|
+
return;
|
|
1325
|
+
const updateLayout = () => {
|
|
1326
|
+
const availableWidth = container.clientWidth;
|
|
1327
|
+
if (!availableWidth)
|
|
1328
|
+
return;
|
|
1329
|
+
if (isMobile || availableWidth < 640) {
|
|
1330
|
+
setStackLanguageChips(true);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (stackLanguageChips) {
|
|
1334
|
+
const normalMinWidth = normalTableMinWidthRef.current;
|
|
1335
|
+
if (normalMinWidth && availableWidth >= normalMinWidth) {
|
|
1336
|
+
setStackLanguageChips(false);
|
|
1337
|
+
}
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
const requiredWidth = table.scrollWidth;
|
|
1341
|
+
normalTableMinWidthRef.current = requiredWidth;
|
|
1342
|
+
if (requiredWidth > availableWidth + 1) {
|
|
1343
|
+
setStackLanguageChips(true);
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
updateLayout();
|
|
1347
|
+
const animationFrame = window.requestAnimationFrame(updateLayout);
|
|
1348
|
+
const timeout = window.setTimeout(updateLayout, 150);
|
|
1349
|
+
let observer;
|
|
1350
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
1351
|
+
observer = new ResizeObserver(updateLayout);
|
|
1352
|
+
observer.observe(container);
|
|
1353
|
+
observer.observe(table);
|
|
1354
|
+
}
|
|
1355
|
+
window.addEventListener("resize", updateLayout);
|
|
1356
|
+
return () => {
|
|
1357
|
+
window.cancelAnimationFrame(animationFrame);
|
|
1358
|
+
window.clearTimeout(timeout);
|
|
1359
|
+
observer?.disconnect();
|
|
1360
|
+
window.removeEventListener("resize", updateLayout);
|
|
1361
|
+
};
|
|
1362
|
+
}, [
|
|
1363
|
+
isMobile,
|
|
1364
|
+
stackLanguageChips,
|
|
1365
|
+
statusFilteredJobs,
|
|
1366
|
+
]);
|
|
1069
1367
|
if (isLoading && !jobs) {
|
|
1070
1368
|
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
1369
|
}
|
|
@@ -1083,37 +1381,46 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
|
|
|
1083
1381
|
byItem.set(key, []);
|
|
1084
1382
|
byItem.get(key).push(job);
|
|
1085
1383
|
}
|
|
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
|
-
|
|
1384
|
+
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) => {
|
|
1385
|
+
const isItemExpanded = expandedItems.has(itemId);
|
|
1386
|
+
const itemAnyInProgress = itemJobs.some((j) => j.status === "In Progress");
|
|
1387
|
+
const itemAnyPending = itemJobs.some((j) => j.status === "Pending");
|
|
1388
|
+
const itemIsQueued = !itemAnyInProgress && itemAnyPending;
|
|
1389
|
+
const status = itemStatusDot(itemJobs);
|
|
1390
|
+
const firstJob = itemJobs[0];
|
|
1391
|
+
const itemPath = firstJob?.itemPath ?? null;
|
|
1392
|
+
const serverName = firstJob?.itemName ?? null;
|
|
1393
|
+
const metadataName = parseItemName(firstJob?.metadata, "");
|
|
1394
|
+
const displayName = serverName || metadataName || itemId;
|
|
1395
|
+
const titleAttr = `${itemPath ?? ""}\n${itemId}`.trim();
|
|
1396
|
+
const itemErrorJobs = itemJobs.filter(isRetriableJob);
|
|
1397
|
+
const itemRetryKey = `item:${batchId}:${itemId}`;
|
|
1398
|
+
const isRetryingItem = retryingKeys.has(itemRetryKey);
|
|
1399
|
+
const rowLanguages = itemJobs
|
|
1400
|
+
.map((job) => job.targetLanguage || job.sourceLanguage)
|
|
1401
|
+
.filter((language) => Boolean(language));
|
|
1402
|
+
const normalizedCurrentLanguage = normalizeLanguageCode(currentLanguage);
|
|
1403
|
+
const rowOpenLanguage = rowLanguages.find((language) => normalizeLanguageCode(language) === normalizedCurrentLanguage) ?? rowLanguages[0];
|
|
1404
|
+
const languageChips = (_jsx(InlineLanguageJobChips, { batchId: batchId, batchProvider: batchProvider, jobs: itemJobs, languageMap: languageMap, isExpanded: isItemExpanded, batchStartedAtUtc: batchStartedAtUtc, retryingKeys: retryingKeys, singleLine: !stackLanguageChips, onToggle: () => onToggleItem(itemId), onOpenItem: onOpenItem, onRetryJobs: onRetryJobs }));
|
|
1405
|
+
const showRetryAction = itemErrorJobs.length > 0;
|
|
1406
|
+
const retryAction = showRetryAction ? (_jsx("div", { className: "flex shrink-0 items-center justify-end", children: _jsx(SimpleIconButton, { onClick: (event) => {
|
|
1407
|
+
event.stopPropagation();
|
|
1408
|
+
if (itemErrorJobs.length === 0)
|
|
1409
|
+
return;
|
|
1410
|
+
void onRetryJobs(batchId, batchProvider, itemErrorJobs, itemRetryKey);
|
|
1411
|
+
}, 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;
|
|
1412
|
+
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
|
|
1413
|
+
? { overflowWrap: "anywhere" }
|
|
1414
|
+
: 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 }))] })] }));
|
|
1415
|
+
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: () => {
|
|
1416
|
+
void onOpenItem(itemId, rowOpenLanguage);
|
|
1417
|
+
}, onKeyDown: (e) => {
|
|
1418
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
1419
|
+
e.preventDefault();
|
|
1420
|
+
void onOpenItem(itemId, rowOpenLanguage);
|
|
1421
|
+
}
|
|
1422
|
+
}, 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));
|
|
1423
|
+
}) })] }) }));
|
|
1117
1424
|
}
|
|
1118
1425
|
function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying = false, onRetry, }) {
|
|
1119
1426
|
const [open, setOpen] = React.useState(false);
|
|
@@ -1131,16 +1438,16 @@ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying
|
|
|
1131
1438
|
? "border-highlight-100/30 bg-highlight-10 text-highlight-100 hover:brightness-95"
|
|
1132
1439
|
: jobPending
|
|
1133
1440
|
? "border-border-default bg-neutral-grey-5 text-neutral-grey-50 hover:bg-neutral-grey-5"
|
|
1134
|
-
: "border-
|
|
1441
|
+
: "border-feedback-green bg-feedback-green-light text-feedback-green hover:border-feedback-green hover:brightness-95";
|
|
1135
1442
|
const statusBadgeCls = jobError
|
|
1136
|
-
? "bg-feedback-red-light text-feedback-red
|
|
1443
|
+
? "bg-feedback-red-light text-feedback-red"
|
|
1137
1444
|
: jobAborted
|
|
1138
|
-
? "bg-feedback-orange-light text-feedback-orange
|
|
1445
|
+
? "bg-feedback-orange-light text-feedback-orange"
|
|
1139
1446
|
: jobInProgress
|
|
1140
|
-
? "bg-highlight-10 text-highlight-100
|
|
1447
|
+
? "bg-highlight-10 text-highlight-100"
|
|
1141
1448
|
: jobPending
|
|
1142
|
-
? "bg-neutral-grey-5 text-neutral-grey-50
|
|
1143
|
-
: "bg-feedback-green-light text-feedback-green
|
|
1449
|
+
? "bg-neutral-grey-5 text-neutral-grey-50"
|
|
1450
|
+
: "bg-feedback-green-light text-feedback-green";
|
|
1144
1451
|
const timestamp = job.timestamp ? new Date(job.timestamp) : null;
|
|
1145
1452
|
const timestampValid = timestamp && !isNaN(timestamp.getTime());
|
|
1146
1453
|
const claimedAt = job.claimedAtUtc ? new Date(job.claimedAtUtc) : null;
|
|
@@ -1198,9 +1505,9 @@ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying
|
|
|
1198
1505
|
const emptyFieldText = statistics && statistics.emptyFieldCount > 0
|
|
1199
1506
|
? `${formatCount(statistics.emptyFieldCount)} empty`
|
|
1200
1507
|
: 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
|
|
1508
|
+
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
1509
|
? "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
|
|
1510
|
+
: 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
1511
|
(jobError || jobAborted ? (
|
|
1205
1512
|
// Error/abort messages can be long and multi-line (e.g. an external
|
|
1206
1513
|
// ITranslationService stack trace). Render them in a bounded,
|