@parhelia/localization 0.4.12796 → 0.4.12818
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/LocalizeItemCommand.d.ts.map +1 -1
- package/dist/LocalizeItemCommand.js +5 -0
- package/dist/hooks/useTranslationWizard.d.ts.map +1 -1
- package/dist/hooks/useTranslationWizard.js +1 -0
- package/dist/steps/ItemSelectionTree.d.ts.map +1 -1
- package/dist/steps/ItemSelectionTree.js +26 -4
- package/dist/steps/ProviderStep.js +1 -1
- package/dist/steps/ServiceLanguageSelectionStep.d.ts.map +1 -1
- package/dist/steps/ServiceLanguageSelectionStep.js +31 -13
- package/dist/steps/types.d.ts +2 -0
- package/dist/steps/types.d.ts.map +1 -1
- package/dist/translation-center/TranslationBatches.d.ts.map +1 -1
- package/dist/translation-center/TranslationBatches.js +175 -56
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalizeItemCommand.d.ts","sourceRoot":"","sources":["../src/LocalizeItemCommand.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,OAAO,EACP,cAAc,EACd,WAAW,EACX,eAAe,EACf,QAAQ,EACT,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAEL,uBAAuB,EACxB,MAAM,eAAe,CAAC;AAQvB,MAAM,MAAM,uBAAuB,GAAG,WAAW,GAAG;IAClD,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GACpC,cAAc,CAAC,uBAAuB,CAAC,CAAC;AAC1C,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAEnE,wBAAsB,sBAAsB,CAAC,EAC3C,WAAW,EACX,UAAmC,EACnC,KAAK,GACN,EAAE;IACD,WAAW,EAAE,eAAe,CAAC;IAC7B,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC3C,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,2CAiCA;AAED,eAAO,MAAM,mBAAmB,EAAE,
|
|
1
|
+
{"version":3,"file":"LocalizeItemCommand.d.ts","sourceRoot":"","sources":["../src/LocalizeItemCommand.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,OAAO,EACP,cAAc,EACd,WAAW,EACX,eAAe,EACf,QAAQ,EACT,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EAEL,uBAAuB,EACxB,MAAM,eAAe,CAAC;AAQvB,MAAM,MAAM,uBAAuB,GAAG,WAAW,GAAG;IAClD,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,0BAA0B,GACpC,cAAc,CAAC,uBAAuB,CAAC,CAAC;AAC1C,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;AAEnE,wBAAsB,sBAAsB,CAAC,EAC3C,WAAW,EACX,UAAmC,EACnC,KAAK,GACN,EAAE;IACD,WAAW,EAAE,eAAe,CAAC;IAC7B,UAAU,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC3C,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,2CAiCA;AAED,eAAO,MAAM,mBAAmB,EAAE,mBAkCjC,CAAC"}
|
|
@@ -39,6 +39,11 @@ export const localizeItemCommand = {
|
|
|
39
39
|
if (!context.data?.items || context.data.items.length === 0) {
|
|
40
40
|
return true;
|
|
41
41
|
}
|
|
42
|
+
// Translation writes the target versions, so a selection where the user
|
|
43
|
+
// cannot write any item has nothing the wizard could do.
|
|
44
|
+
if (context.data.items.every((item) => item.canWriteItem === false)) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
42
47
|
// Check if multi-item is disabled and multiple items are selected
|
|
43
48
|
const multiItemEnabled = context.editContext?.configuration?.localization?.multiItem !==
|
|
44
49
|
false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTranslationWizard.d.ts","sourceRoot":"","sources":["../../src/hooks/useTranslationWizard.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAUhF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,uBAAuB;;;
|
|
1
|
+
{"version":3,"file":"useTranslationWizard.d.ts","sourceRoot":"","sources":["../../src/hooks/useTranslationWizard.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAUhF,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,uBAAuB;;;EAuGlE"}
|
|
@@ -45,6 +45,7 @@ export function useTranslationWizard(props) {
|
|
|
45
45
|
name: language.name,
|
|
46
46
|
languageCode: language.languageCode,
|
|
47
47
|
items: [],
|
|
48
|
+
canWrite: language.canWrite,
|
|
48
49
|
};
|
|
49
50
|
if (language.versions > 0 && itemDescriptors[itemIndex]) {
|
|
50
51
|
languageData.items.push(itemDescriptors[itemIndex]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ItemSelectionTree.d.ts","sourceRoot":"","sources":["../../src/steps/ItemSelectionTree.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAMrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAGrD,KAAK,sBAAsB,GAAG;IAC5B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,OAAO,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAC/C,WAAW,EAAE,eAAe,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B,CAAC;
|
|
1
|
+
{"version":3,"file":"ItemSelectionTree.d.ts","sourceRoot":"","sources":["../../src/steps/ItemSelectionTree.tsx"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,eAAe,EAMrB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAGrD,KAAK,sBAAsB,GAAG;IAC5B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,OAAO,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAC/C,WAAW,EAAE,eAAe,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC1B,CAAC;AAuFF,wBAAgB,iBAAiB,CAAC,EAChC,IAAI,EACJ,OAAO,EACP,WAAW,EACX,QAAe,EACf,sBAAsB,EACtB,MAAY,GACb,EAAE,sBAAsB,+BA0WxB"}
|
|
@@ -2,7 +2,20 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { ItemCollectionEditor, } from "@parhelia/core";
|
|
4
4
|
import { streamSubitemCount } from "../api/discovery";
|
|
5
|
-
const
|
|
5
|
+
const TRANSLATION_ROOT_ITEM_IDS = [
|
|
6
|
+
"0de95ae4-41ab-4d01-9eb0-67441b7c2450", // Content
|
|
7
|
+
"b701850a-cb8a-4943-b2bc-dddb1238c103", // Forms
|
|
8
|
+
"3d6658d8-a0bf-4e75-b3e2-d050fabcf4e1", // Media Library
|
|
9
|
+
];
|
|
10
|
+
const NO_WRITE_ACCESS_TITLE = "You don't have write access to this item, so it can't be translated.";
|
|
11
|
+
/**
|
|
12
|
+
* Only items the user may write can receive a translation (the backend
|
|
13
|
+
* rejects the rest at start time). Sources that don't report the flag are
|
|
14
|
+
* treated as allowed and left to the backend check.
|
|
15
|
+
*/
|
|
16
|
+
function isWritable(item) {
|
|
17
|
+
return item.canWrite !== false && item.canWriteItem !== false;
|
|
18
|
+
}
|
|
6
19
|
function fullItemFromDescriptor(descriptor) {
|
|
7
20
|
return {
|
|
8
21
|
id: descriptor.id,
|
|
@@ -24,7 +37,7 @@ function fullItemFromDescriptor(descriptor) {
|
|
|
24
37
|
thumbnail: "",
|
|
25
38
|
path: descriptor.path ?? "",
|
|
26
39
|
fields: [],
|
|
27
|
-
canWriteItem: false,
|
|
40
|
+
canWriteItem: descriptor.canWrite !== false,
|
|
28
41
|
canWriteLanguage: false,
|
|
29
42
|
canWriteWorkflow: false,
|
|
30
43
|
canRename: false,
|
|
@@ -68,7 +81,11 @@ export function ItemSelectionTree({ data, setData, editContext, isActive = true,
|
|
|
68
81
|
const seed = data.discoveredItems && data.discoveredItems.length > 0
|
|
69
82
|
? data.discoveredItems
|
|
70
83
|
: data.items;
|
|
71
|
-
|
|
84
|
+
// The command can be invoked on a read-only item (context menu, sidebar);
|
|
85
|
+
// such seeds are dropped so the list never holds an untranslatable item.
|
|
86
|
+
return seed
|
|
87
|
+
.filter(isWritable)
|
|
88
|
+
.map((item) => fullItemToWithSubtree(item, !!data.includeSubitems));
|
|
72
89
|
// First mount only — once the user edits, wizard data is downstream.
|
|
73
90
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
91
|
}, []);
|
|
@@ -254,6 +271,7 @@ export function ItemSelectionTree({ data, setData, editContext, isActive = true,
|
|
|
254
271
|
icon: anyNode.icon,
|
|
255
272
|
templateId: anyNode.templateId,
|
|
256
273
|
hasChildren: anyNode.hasChildren,
|
|
274
|
+
canWrite: anyNode.canWrite,
|
|
257
275
|
}));
|
|
258
276
|
}, [language]);
|
|
259
277
|
const handleAddToList = useCallback(async (itemsToAdd) => {
|
|
@@ -265,6 +283,8 @@ export function ItemSelectionTree({ data, setData, editContext, isActive = true,
|
|
|
265
283
|
for (const node of source) {
|
|
266
284
|
if (!node.id || existing.has(node.id))
|
|
267
285
|
continue;
|
|
286
|
+
if (!isWritable(node))
|
|
287
|
+
continue;
|
|
268
288
|
const nodeWithIcon = node;
|
|
269
289
|
cacheTreeNode(node);
|
|
270
290
|
next.push({
|
|
@@ -290,6 +310,8 @@ export function ItemSelectionTree({ data, setData, editContext, isActive = true,
|
|
|
290
310
|
const id = item.id;
|
|
291
311
|
if (!id || items.some((i) => i.descriptor.id === id))
|
|
292
312
|
return;
|
|
313
|
+
if (!isWritable(item))
|
|
314
|
+
return;
|
|
293
315
|
cacheTreeNode(item);
|
|
294
316
|
const next = [
|
|
295
317
|
...items,
|
|
@@ -328,5 +350,5 @@ export function ItemSelectionTree({ data, setData, editContext, isActive = true,
|
|
|
328
350
|
setItems(next);
|
|
329
351
|
commitToWizard(next);
|
|
330
352
|
}, [items, commitToWizard]);
|
|
331
|
-
return (_jsx(ItemCollectionEditor, { items: items, selectedInTree: selectedInTree, onSelectedInTreeChange: setSelectedInTree, selectedFromList: selectedFromList, onSelectedFromListChange: setSelectedFromList, onAddToList: handleAddToList, onRemoveFromList: handleRemoveFromList, onAddItem: handleAddItem, onRemoveItem: handleRemoveItem, onToggleSubitems: handleToggleSubitems, language: language, rootItemIds:
|
|
353
|
+
return (_jsx(ItemCollectionEditor, { items: items, selectedInTree: selectedInTree, onSelectedInTreeChange: setSelectedInTree, selectedFromList: selectedFromList, onSelectedFromListChange: setSelectedFromList, onAddToList: handleAddToList, onRemoveFromList: handleRemoveFromList, onAddItem: handleAddItem, onRemoveItem: handleRemoveItem, onToggleSubitems: handleToggleSubitems, language: language, rootItemIds: TRANSLATION_ROOT_ITEM_IDS, hideRootNodes: false, isItemSelectable: isWritable, unselectableItemTitle: NO_WRITE_ACCESS_TITLE, selectedItemsLabel: "Items to Translate", emptyMessage: "No items selected", emptyHint: "Browse or search to add items", height: height, subitemCounts: subitemCounts, expandIdPath: expandIdPath, localStorageKey: "translation-wizard.itemCollectionSplitter", treeSearchContainerClassName: "border-b-0 p-0 pb-3", frameless: true }));
|
|
332
354
|
}
|
|
@@ -228,5 +228,5 @@ export function ProviderStep({ isActive = true, data, setData, onStepCompleted,
|
|
|
228
228
|
}), options: data.translationProviders.map((provider) => ({
|
|
229
229
|
value: provider.name,
|
|
230
230
|
label: provider.displayName || provider.name,
|
|
231
|
-
})), placeholder: "Select a provider...", size: "sm", className: "mt-2 w-full", "data-testid": "translation-provider-select" }), isCreateLanguageVersionsOnlyProvider && (_jsx("p", { className: "text-muted-foreground mt-2 text-xs", children: "\"Create Language Versions Only\" will create new language versions without automatic translation." }))] }), supportsPromptCustomization && (_jsxs("div", { children: [_jsxs(Label, { className: "flex cursor-pointer items-start gap-2.5", children: [_jsx("input", { type: "checkbox", checked: enableCustomPrompt, onChange: (e) => handleCustomPromptToggle(e.target.checked), className: "mt-0.5 h-3.5 w-3.5 rounded border-border-default text-[var(--color-highlight-100)] accent-[var(--color-highlight-100)] focus:ring-[var(--color-highlight-100)]", "data-testid": "enable-custom-prompt-checkbox" }), _jsxs("div", { className: "min-w-0", children: [_jsx("span", { className: "block text-[13px] font-medium text-neutral-grey-100", children: "Customize translation prompt (optional)" }), _jsx("span", { className: "text-muted-foreground mt-0.5 block text-xs", children: "Edit the provider prompt before starting translation." })] })] }), enableCustomPrompt && (_jsxs("div", { className: "mt-4 space-y-2", children: [_jsx(Textarea, { id: "custom-prompt-textarea", value: customPrompt, onChange: (e) => setCustomPrompt(e.target.value), className: "text-xs", rows: 8, placeholder: "
|
|
231
|
+
})), placeholder: "Select a provider...", size: "sm", className: "mt-2 w-full", "data-testid": "translation-provider-select" }), isCreateLanguageVersionsOnlyProvider && (_jsx("p", { className: "text-muted-foreground mt-2 text-xs", children: "\"Create Language Versions Only\" will create new language versions without automatic translation." }))] }), supportsPromptCustomization && (_jsxs("div", { children: [_jsxs(Label, { className: "flex cursor-pointer items-start gap-2.5", children: [_jsx("input", { type: "checkbox", checked: enableCustomPrompt, onChange: (e) => handleCustomPromptToggle(e.target.checked), className: "mt-0.5 h-3.5 w-3.5 rounded border-border-default text-[var(--color-highlight-100)] accent-[var(--color-highlight-100)] focus:ring-[var(--color-highlight-100)]", "data-testid": "enable-custom-prompt-checkbox" }), _jsxs("div", { className: "min-w-0", children: [_jsx("span", { className: "block text-[13px] font-medium text-neutral-grey-100", children: "Customize translation prompt (optional)" }), _jsx("span", { className: "text-muted-foreground mt-0.5 block text-xs", children: "Edit the provider prompt before starting translation." })] })] }), enableCustomPrompt && (_jsxs("div", { className: "mt-4 space-y-2", children: [_jsx(Textarea, { id: "custom-prompt-textarea", value: customPrompt, onChange: (e) => setCustomPrompt(e.target.value), className: "text-xs", rows: 8, placeholder: "Add instructions for the translator, e.g. tone of voice, terms to keep untranslated, or formality.", "data-testid": "custom-prompt-textarea" }), hasDefaultPrompt && (_jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: handleResetPrompt, disabled: customPrompt === (defaultPrompt || ""), className: "gap-1.5", title: "Reset to the default prompt", "data-testid": "reset-custom-prompt-button", children: [_jsx(ResetIcon, { className: "size-3.5", strokeWidth: 1, "aria-hidden": "true" }), "Reset to default"] }))] }))] }))] })] }) }) }));
|
|
232
232
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServiceLanguageSelectionStep.d.ts","sourceRoot":"","sources":["../../src/steps/ServiceLanguageSelectionStep.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAyB,MAAM,SAAS,CAAC;AAGtE;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,QAAe,EACf,IAAI,EACJ,OAAO,EACP,eAAe,EACf,WAAW,GACZ,EAAE,oBAAoB,+
|
|
1
|
+
{"version":3,"file":"ServiceLanguageSelectionStep.d.ts","sourceRoot":"","sources":["../../src/steps/ServiceLanguageSelectionStep.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAyB,MAAM,SAAS,CAAC;AAGtE;;;;GAIG;AACH,wBAAgB,4BAA4B,CAAC,EAC3C,QAAe,EACf,IAAI,EACJ,OAAO,EACP,eAAe,EACf,WAAW,GACZ,EAAE,oBAAoB,+BAkUtB"}
|
|
@@ -82,6 +82,12 @@ export function ServiceLanguageSelectionStep({ isActive = true, data, setData, o
|
|
|
82
82
|
data.languageData.get(sourceLanguage)?.name ||
|
|
83
83
|
siteLanguageMap.get(sourceLanguage)?.name ||
|
|
84
84
|
sourceLanguage;
|
|
85
|
+
// Language write ACL (language:write). Every source reports it for
|
|
86
|
+
// the same language item, so the first defined value wins; unknown
|
|
87
|
+
// is treated as allowed and left to the backend check.
|
|
88
|
+
const canWrite = editContextLang?.canWrite ??
|
|
89
|
+
languageDataLang?.canWrite ??
|
|
90
|
+
siteLanguage?.canWrite;
|
|
85
91
|
return {
|
|
86
92
|
code,
|
|
87
93
|
name: editContextLang?.name ||
|
|
@@ -91,6 +97,7 @@ export function ServiceLanguageSelectionStep({ isActive = true, data, setData, o
|
|
|
91
97
|
icon: editContextLang?.icon || siteLanguage?.icon,
|
|
92
98
|
sourceLanguage,
|
|
93
99
|
sourceLanguageName,
|
|
100
|
+
canWrite: canWrite !== false,
|
|
94
101
|
};
|
|
95
102
|
})
|
|
96
103
|
.filter((lang) => lang.sourceLanguage !== lang.code);
|
|
@@ -114,7 +121,8 @@ export function ServiceLanguageSelectionStep({ isActive = true, data, setData, o
|
|
|
114
121
|
const initialSelection = {};
|
|
115
122
|
for (const langCode of data.targetLanguages) {
|
|
116
123
|
const lang = allLanguages.find((l) => l.code === langCode);
|
|
117
|
-
|
|
124
|
+
// Predefined targets the user cannot write are dropped silently.
|
|
125
|
+
if (lang?.canWrite)
|
|
118
126
|
initialSelection[langCode] = true;
|
|
119
127
|
}
|
|
120
128
|
setLanguageSelection(initialSelection);
|
|
@@ -137,37 +145,47 @@ export function ServiceLanguageSelectionStep({ isActive = true, data, setData, o
|
|
|
137
145
|
};
|
|
138
146
|
setData(newData);
|
|
139
147
|
}, [languageSelection, setData, data.targetLanguages]);
|
|
148
|
+
// Only languages the user may write take part in toggling / select-all;
|
|
149
|
+
// the others render disabled below.
|
|
150
|
+
const writableLanguages = useMemo(() => allLanguages.filter((lang) => lang.canWrite), [allLanguages]);
|
|
140
151
|
const handleLanguageToggle = (langCode) => {
|
|
152
|
+
if (!writableLanguages.some((lang) => lang.code === langCode))
|
|
153
|
+
return;
|
|
141
154
|
setLanguageSelection((prev) => ({ ...prev, [langCode]: !prev[langCode] }));
|
|
142
155
|
};
|
|
143
156
|
const handleSelectAllLanguages = () => {
|
|
144
|
-
const allSelected =
|
|
157
|
+
const allSelected = writableLanguages.every((lang) => languageSelection[lang.code]);
|
|
145
158
|
const newSelection = {};
|
|
146
|
-
|
|
159
|
+
writableLanguages.forEach((lang) => {
|
|
147
160
|
newSelection[lang.code] = !allSelected;
|
|
148
161
|
});
|
|
149
162
|
setLanguageSelection(newSelection);
|
|
150
163
|
};
|
|
151
164
|
const areAllLanguagesSelected = useMemo(() => {
|
|
152
|
-
if (
|
|
165
|
+
if (writableLanguages.length === 0)
|
|
153
166
|
return false;
|
|
154
|
-
return
|
|
155
|
-
}, [
|
|
167
|
+
return writableLanguages.every((lang) => languageSelection[lang.code]);
|
|
168
|
+
}, [writableLanguages, languageSelection]);
|
|
156
169
|
const areSomeLanguagesSelected = useMemo(() => {
|
|
157
|
-
return
|
|
158
|
-
}, [
|
|
170
|
+
return writableLanguages.some((lang) => languageSelection[lang.code]);
|
|
171
|
+
}, [writableLanguages, languageSelection]);
|
|
159
172
|
const selectedCount = data.targetLanguages.length;
|
|
160
|
-
return (_jsx(WizardStepShell, { fillHeight: true, noPadding: true, testId: "language-selection-step", children: _jsxs("div", { className: "flex min-h-0 w-full flex-1 flex-col overflow-hidden p-6", children: [_jsxs("div", { className: "flex shrink-0 items-start justify-between gap-2 pb-3", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "text-xs font-medium tracking-wider text-neutral-grey-50", children: "Select target languages" }), selectedCount > 0 && _jsx(CountBadge, { children: selectedCount })] }),
|
|
173
|
+
return (_jsx(WizardStepShell, { fillHeight: true, noPadding: true, testId: "language-selection-step", children: _jsxs("div", { className: "flex min-h-0 w-full flex-1 flex-col overflow-hidden p-6", children: [_jsxs("div", { className: "flex shrink-0 items-start justify-between gap-2 pb-3", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "text-xs font-medium tracking-wider text-neutral-grey-50", children: "Select target languages" }), selectedCount > 0 && _jsx(CountBadge, { children: selectedCount })] }), writableLanguages.length > 1 && (_jsxs("label", { className: "flex cursor-pointer items-center gap-1.5 text-[11px] font-medium text-neutral-grey-50 transition-colors hover:text-neutral-grey-100", children: [_jsx("input", { type: "checkbox", checked: areAllLanguagesSelected, ref: (input) => {
|
|
161
174
|
if (input) {
|
|
162
175
|
input.indeterminate =
|
|
163
176
|
areSomeLanguagesSelected && !areAllLanguagesSelected;
|
|
164
177
|
}
|
|
165
178
|
}, onChange: handleSelectAllLanguages, className: "h-3 w-3 rounded border-border-default text-[var(--color-highlight-100)] accent-[var(--color-highlight-100)] focus:ring-[var(--color-highlight-100)]", "data-testid": "select-all-languages-checkbox" }), "Select all"] }))] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto pt-2", children: allLanguages.length === 0 ? (_jsx("div", { className: "grid gap-2.5 sm:grid-cols-2", children: [...Array(6)].map((_, i) => (_jsxs("div", { className: "flex animate-pulse items-center gap-2 rounded-[8px] border border-border-default px-3 py-2", children: [_jsx("div", { className: "h-3.5 w-3.5 rounded bg-neutral-grey-10" }), _jsx("div", { className: "h-4 w-4 bg-neutral-grey-10" }), _jsx("div", { className: "h-3.5 w-24 rounded bg-neutral-grey-10" }), _jsx("div", { className: "ml-auto h-3 w-10 rounded bg-neutral-grey-5" })] }, i))) })) : (_jsx("div", { className: "grid gap-2.5 sm:grid-cols-2", children: allLanguages.map((lang) => {
|
|
166
179
|
const checked = !!languageSelection[lang.code];
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
180
|
+
const readOnly = !lang.canWrite;
|
|
181
|
+
return (_jsxs("label", { title: readOnly
|
|
182
|
+
? "You don't have write access to this language."
|
|
183
|
+
: undefined, "data-testid": `language-option-${lang.code}`, "data-readonly": readOnly || undefined, className: `group flex items-center gap-2.5 rounded-[8px] border px-2 py-1.5 transition-all ${readOnly
|
|
184
|
+
? "cursor-not-allowed border-border-default opacity-50"
|
|
185
|
+
: checked
|
|
186
|
+
? "cursor-pointer border-neutral-grey-100"
|
|
187
|
+
: "cursor-pointer border-border-default hover:border-border-default hover:bg-neutral-grey-5"}`, children: [_jsx("input", { type: "checkbox", checked: checked, disabled: readOnly, onChange: () => handleLanguageToggle(lang.code), className: "h-3.5 w-3.5 shrink-0 rounded border-border-default text-[var(--color-highlight-100)] accent-[var(--color-highlight-100)] focus:ring-[var(--color-highlight-100)] disabled:cursor-not-allowed", "data-testid": `language-checkbox-${lang.code}` }), lang.icon ? (_jsx("img", { src: lang.icon, alt: "", "aria-hidden": "true", className: "h-4 w-5 shrink-0 object-cover" })) : (_jsx("span", { className: "inline-block h-4 w-5 shrink-0 bg-neutral-grey-5" })), _jsx("span", { className: `min-w-0 flex-1 truncate text-[13px] ${checked
|
|
170
188
|
? "font-medium text-neutral-grey-100"
|
|
171
|
-
: "text-neutral-grey-100"}`, children: lang.name }), _jsxs("span", { className: "ml-auto flex max-w-[9rem] shrink-0 flex-col items-end leading-tight", children: [_jsx("span", { className: "font-mono text-[10px] tracking-wider text-neutral-grey-50", children: lang.code }), _jsxs("span", { className: "max-w-full truncate text-[10px] text-neutral-grey-50", title: `Source language: ${lang.sourceLanguageName}`, children: ["from ", lang.sourceLanguageName] })] })] }, lang.code));
|
|
189
|
+
: "text-neutral-grey-100"}`, children: lang.name }), _jsxs("span", { className: "ml-auto flex max-w-[9rem] shrink-0 flex-col items-end leading-tight", children: [_jsx("span", { className: "font-mono text-[10px] tracking-wider text-neutral-grey-50", children: lang.code }), readOnly ? (_jsx("span", { className: "max-w-full truncate text-[10px] text-neutral-grey-50", children: "no write access" })) : (_jsxs("span", { className: "max-w-full truncate text-[10px] text-neutral-grey-50", title: `Source language: ${lang.sourceLanguageName}`, children: ["from ", lang.sourceLanguageName] }))] })] }, lang.code));
|
|
172
190
|
}) })) })] }) }));
|
|
173
191
|
}
|
package/dist/steps/types.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type LanguageData = {
|
|
|
20
20
|
languageCode: string;
|
|
21
21
|
items: ItemDescriptor[];
|
|
22
22
|
translationStatus?: TranslationStatus;
|
|
23
|
+
/** Language write ACL (language:write) for the current user. Undefined when not reported. */
|
|
24
|
+
canWrite?: boolean;
|
|
23
25
|
};
|
|
24
26
|
export type TranslationDialogResult = {
|
|
25
27
|
translationProvider: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/steps/types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,cAAc,EACd,eAAe,EACf,eAAe,EACf,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/steps/types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,cAAc,EACd,eAAe,EACf,eAAe,EACf,iBAAiB,EAClB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,uBAAuB,GAAG;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE,MAAM,EAAE,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,EAAE,QAAQ,EAAE,CAAC;IAC5B,kBAAkB,CAAC,EAAE,eAAe,EAAE,CAAC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,yBAAyB,CAAC,EAAE,MAAM,EAAE,CAAC;IACrC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,GAAG,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,oFAAoF;IACpF,eAAe,EAAE,OAAO,CAAC;IACzB,kFAAkF;IAClF,eAAe,EAAE,QAAQ,EAAE,CAAC;IAC5B,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,eAAe,EAAE,CAAC;IACvC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAClD,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACxC,oBAAoB,EAAE,uBAAuB,EAAE,CAAC;IAChD,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/C,QAAQ,CAAC,EAAE,GAAG,CAAC;IACf,iBAAiB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACrC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,OAAO,EAAE,CACP,MAAM,EACF,qBAAqB,GACrB,CAAC,CAAC,OAAO,EAAE,qBAAqB,KAAK,qBAAqB,CAAC,KAC5D,IAAI,CAAC;IACV,WAAW,EAAE,eAAe,CAAC;IAC7B,eAAe,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;IAC9C,qBAAqB,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5E,gBAAgB,CAAC,EAAE,CACjB,OAAO,EAAE;QACP,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,OAAO,EAAE,MAAM,IAAI,CAAC;QACpB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,EAAE,KACA,IAAI,CAAC;IACV,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI,KAAK,IAAI,CAAC;CACjE,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,WAAW,EAAE,eAAe,CAAC;IAC7B,aAAa,CAAC,EAAE,8BAA8B,CAAC;CAChD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TranslationBatches.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationBatches.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,MAAM,OAAO,CAAC;AAyV1B,wBAAgB,kBAAkB,
|
|
1
|
+
{"version":3,"file":"TranslationBatches.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationBatches.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,MAAM,OAAO,CAAC;AAyV1B,wBAAgB,kBAAkB,sBA6qCjC;AAED,eAAO,MAAM,oCAAoC,8BAA8B,CAAC;AAEhF,wBAAgB,8BAA8B,sBA+Y7C"}
|
|
@@ -191,6 +191,11 @@ export function TranslationBatches() {
|
|
|
191
191
|
const [batches, setBatches] = useState([]);
|
|
192
192
|
const [sitecoreLanguages, setSitecoreLanguages] = useState([]);
|
|
193
193
|
const [isLoading, setIsLoading] = useState(false);
|
|
194
|
+
// False until the first list response lands. Later reloads (the user filter
|
|
195
|
+
// defaulting in, socket refreshes, the reconciliation poll) update in place
|
|
196
|
+
// instead of swapping the view for a spinner: the home view picks its slogan
|
|
197
|
+
// per mount, so every swap re-rolled the greeting.
|
|
198
|
+
const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
|
|
194
199
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
195
200
|
const [hasMore, setHasMore] = useState(true);
|
|
196
201
|
const [currentOffset, setCurrentOffset] = useState(0);
|
|
@@ -209,7 +214,11 @@ export function TranslationBatches() {
|
|
|
209
214
|
// (e.g. when the user changes filters quickly).
|
|
210
215
|
const requestIdRef = useRef(0);
|
|
211
216
|
const currentUserName = editContext?.user?.name || "all";
|
|
212
|
-
|
|
217
|
+
// Limited-preview sessions and accounts confined to their own batches (see
|
|
218
|
+
// userInfo.canViewAllTranslationBatches) get the user filter pinned to themselves;
|
|
219
|
+
// the server enforces the same scope on every batch endpoint.
|
|
220
|
+
const userFilterLocked = (editContext?.user?.isLimitedPreviewUser ?? false) ||
|
|
221
|
+
editContext?.userInfo?.canViewAllTranslationBatches === false;
|
|
213
222
|
const isMobile = editContext?.isMobile ?? false;
|
|
214
223
|
const rootRef = useRef(null);
|
|
215
224
|
const [isCompactBatches, setIsCompactBatches] = useState(isMobile);
|
|
@@ -253,7 +262,7 @@ export function TranslationBatches() {
|
|
|
253
262
|
}, [currentUserName]);
|
|
254
263
|
// Enforce current user filter for limited preview users
|
|
255
264
|
useEffect(() => {
|
|
256
|
-
if (
|
|
265
|
+
if (userFilterLocked &&
|
|
257
266
|
currentUserName !== "all" &&
|
|
258
267
|
filters.user !== currentUserName) {
|
|
259
268
|
setTranslationBatchFilters((prev) => ({
|
|
@@ -261,9 +270,9 @@ export function TranslationBatches() {
|
|
|
261
270
|
user: currentUserName,
|
|
262
271
|
}));
|
|
263
272
|
}
|
|
264
|
-
}, [
|
|
273
|
+
}, [userFilterLocked, currentUserName, filters.user]);
|
|
265
274
|
const effectiveFilters = useMemo(() => {
|
|
266
|
-
const userFilter =
|
|
275
|
+
const userFilter = userFilterLocked ? currentUserName : filters.user;
|
|
267
276
|
const trimmedItem = filters.itemIdOrPath.trim();
|
|
268
277
|
return {
|
|
269
278
|
fromUtc: dateRangeToFromUtc(filters.dateRange),
|
|
@@ -276,7 +285,7 @@ export function TranslationBatches() {
|
|
|
276
285
|
itemIdOrPath: trimmedItem ? trimmedItem : undefined,
|
|
277
286
|
itemIncludeSubitems: trimmedItem && filters.itemIncludeSubitems ? true : undefined,
|
|
278
287
|
};
|
|
279
|
-
}, [filters,
|
|
288
|
+
}, [filters, userFilterLocked, currentUserName]);
|
|
280
289
|
const loadRecentBatches = useCallback(async (offset, append, filtersToUse) => {
|
|
281
290
|
const myRequestId = ++requestIdRef.current;
|
|
282
291
|
if (offset === 0) {
|
|
@@ -323,6 +332,7 @@ export function TranslationBatches() {
|
|
|
323
332
|
if (myRequestId === requestIdRef.current) {
|
|
324
333
|
setIsLoading(false);
|
|
325
334
|
setIsLoadingMore(false);
|
|
335
|
+
setHasLoadedOnce(true);
|
|
326
336
|
}
|
|
327
337
|
}
|
|
328
338
|
}, []);
|
|
@@ -777,12 +787,18 @@ export function TranslationBatches() {
|
|
|
777
787
|
editContext?.setShowAgentsWorkspaceEditor(true);
|
|
778
788
|
await editContext?.loadItem(language ? { id: normalized, language, version: 0 } : normalized);
|
|
779
789
|
}, [editContext]);
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
790
|
+
// The blank-slate home view (no header, grey full-height greeting) also
|
|
791
|
+
// covers the initial load, so the greeting mounts exactly once.
|
|
792
|
+
const showHomeView = groupedBatches.length === 0 && !hasActiveEmptyStateFilters;
|
|
793
|
+
// Only the very first load, or a filtered reload with nothing to show,
|
|
794
|
+
// replaces the content with a spinner.
|
|
795
|
+
const showLoadingPlaceholder = !hasLoadedOnce ||
|
|
796
|
+
(isLoading && batches.length === 0 && hasActiveEmptyStateFilters);
|
|
783
797
|
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: [!showHomeView && (_jsx(PageHeader, { title: "Translation Batches", description: isMobile
|
|
784
798
|
? undefined
|
|
785
|
-
: "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 ${showHomeView ? "" : isCompactBatches ? "p-3" : "p-6"}`, children:
|
|
799
|
+
: "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 ${showHomeView ? "" : isCompactBatches ? "p-3" : "p-6"}`, children: showLoadingPlaceholder ? (_jsx("div", { className: showHomeView
|
|
800
|
+
? "bg-neutral-grey-5 flex h-full min-h-[420px] w-full items-center justify-center"
|
|
801
|
+
: "flex items-center justify-center h-32", "data-testid": "translation-batches-loading", children: _jsxs("div", { className: "flex items-center gap-2 text-sm text-neutral-grey-50", children: [_jsx(LoaderIcon, { className: "h-5 w-5 animate-spin text-highlight-100" }), "Loading recent translations..."] }) })) : groupedBatches.length === 0 ? (hasActiveEmptyStateFilters ? (_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: emptyStateSlogan }), _jsx("p", { className: "text-sm mt-1", children: "Try adjusting your filters or start a translation to see it appear here" })] }) })) : (_jsx(WorkspaceHomeView, { slogans: EMPTY_STATE_SLOGANS, actionLabel: "Start your first translation", onAction: editContext ? handleStartFirstTranslation : undefined, testId: "translation-batches-home-view", actionTestId: "translation-batches-start-translation-card" }))) : (_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) => {
|
|
786
802
|
const batchDate = new Date(b.timestamp);
|
|
787
803
|
const formattedBatchDate = formatDateTime(batchDate);
|
|
788
804
|
const info = b.info;
|
|
@@ -798,8 +814,13 @@ export function TranslationBatches() {
|
|
|
798
814
|
const anyError = errorJobs > 0;
|
|
799
815
|
const anyInProgress = isActiveBatchStatus(info?.status);
|
|
800
816
|
const anyAborted = info?.status === "Aborted";
|
|
817
|
+
const jobs = batchJobs[b.id];
|
|
818
|
+
const statusCounts = computeBatchStatusCounts(info, jobs);
|
|
819
|
+
const processedJobs = statusCounts.succeeded +
|
|
820
|
+
statusCounts.failed +
|
|
821
|
+
statusCounts.aborted;
|
|
801
822
|
const metaParts = [];
|
|
802
|
-
metaParts.push(
|
|
823
|
+
metaParts.push(_jsx("span", { className: "text-neutral-grey-100", children: info?.status === "Pending" ? (_jsxs(_Fragment, { children: [totalJobs, " translation", totalJobs !== 1 ? "s" : "", " queued"] })) : anyInProgress ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "tabular-nums", children: [processedJobs, " of ", totalJobs] }), " ", "translation", totalJobs !== 1 ? "s" : "", " done"] })) : (_jsxs(_Fragment, { children: [totalJobs, " translation", totalJobs !== 1 ? "s" : ""] })) }, "trans"));
|
|
803
824
|
if (typeof itemsCount === "number") {
|
|
804
825
|
metaParts.push(_jsxs("span", { children: [itemsCount, " item", itemsCount !== 1 ? "s" : ""] }, "items"));
|
|
805
826
|
}
|
|
@@ -850,8 +871,6 @@ export function TranslationBatches() {
|
|
|
850
871
|
`Translation Batch · ${formattedBatchDate}`);
|
|
851
872
|
})();
|
|
852
873
|
const isExpanded = expandedBatchId === b.id;
|
|
853
|
-
const jobs = batchJobs[b.id];
|
|
854
|
-
const statusCounts = computeBatchStatusCounts(info, jobs);
|
|
855
874
|
const isLoadingThis = loadingJobs.has(b.id);
|
|
856
875
|
const canRetry = anyError;
|
|
857
876
|
const canAbort = isActiveBatchStatus(info?.status);
|
|
@@ -880,19 +899,19 @@ export function TranslationBatches() {
|
|
|
880
899
|
if (!canAbort)
|
|
881
900
|
return;
|
|
882
901
|
handleAbortBatch(b.id);
|
|
883
|
-
}, icon: _jsx(
|
|
902
|
+
}, icon: isAborting ? (_jsx(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin", strokeWidth: 1 })) : (_jsx(AbortIcon, { className: "h-3.5 w-3.5", strokeWidth: 1 })), label: "Abort translation batch", disabled: !canAbort || isAborting }))] }));
|
|
884
903
|
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) => {
|
|
885
904
|
if (e.key === "Enter" || e.key === " ") {
|
|
886
905
|
e.preventDefault();
|
|
887
906
|
toggleBatch(b.id);
|
|
888
907
|
}
|
|
889
|
-
}, 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, { counts: statusCounts, testId: `batch-progress-${b.id}` })), _jsxs("div", { className: `flex min-w-0 flex-1 ${isCompactBatches
|
|
908
|
+
}, 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, { counts: statusCounts, status: info?.status, testId: `batch-progress-${b.id}` })), _jsxs("div", { className: `flex min-w-0 flex-1 ${isCompactBatches
|
|
890
909
|
? "flex-col gap-2"
|
|
891
910
|
: "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
|
|
892
911
|
? "whitespace-normal break-words leading-5"
|
|
893
912
|
: "truncate"}`, style: isCompactBatches
|
|
894
913
|
? { overflowWrap: "anywhere" }
|
|
895
|
-
: 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, { counts: statusCounts, 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));
|
|
914
|
+
: 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, { counts: statusCounts, status: info?.status, 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));
|
|
896
915
|
}) })] }, 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"] })) }) }))] })) }) })] }));
|
|
897
916
|
}
|
|
898
917
|
export const TRANSLATION_BATCH_FILTERS_SIDEBAR_ID = "translation-batch-filters";
|
|
@@ -903,12 +922,16 @@ export function TranslationBatchFiltersSidebar() {
|
|
|
903
922
|
const [sitecoreLanguages, setSitecoreLanguages] = useState([]);
|
|
904
923
|
const [itemPickerOpen, setItemPickerOpen] = useState(false);
|
|
905
924
|
const currentUserName = editContext?.user?.name || "all";
|
|
906
|
-
|
|
925
|
+
// Limited-preview sessions and accounts confined to their own batches (see
|
|
926
|
+
// userInfo.canViewAllTranslationBatches) get the user filter pinned to themselves;
|
|
927
|
+
// the server enforces the same scope on every batch endpoint.
|
|
928
|
+
const userFilterLocked = (editContext?.user?.isLimitedPreviewUser ?? false) ||
|
|
929
|
+
editContext?.userInfo?.canViewAllTranslationBatches === false;
|
|
907
930
|
useEffect(() => {
|
|
908
931
|
ensureDefaultTranslationBatchUser(currentUserName);
|
|
909
932
|
}, [currentUserName]);
|
|
910
933
|
useEffect(() => {
|
|
911
|
-
if (
|
|
934
|
+
if (userFilterLocked &&
|
|
912
935
|
currentUserName !== "all" &&
|
|
913
936
|
filters.user !== currentUserName) {
|
|
914
937
|
setTranslationBatchFilters((prev) => ({
|
|
@@ -916,7 +939,7 @@ export function TranslationBatchFiltersSidebar() {
|
|
|
916
939
|
user: currentUserName,
|
|
917
940
|
}));
|
|
918
941
|
}
|
|
919
|
-
}, [
|
|
942
|
+
}, [userFilterLocked, currentUserName, filters.user]);
|
|
920
943
|
const searchTranslationUsers = useCallback((query, limit) => searchBackendUsers(query, limit), []);
|
|
921
944
|
const debouncedSetItemFilter = useDebouncedCallback((value) => setTranslationBatchFilters((prev) => ({
|
|
922
945
|
...prev,
|
|
@@ -993,20 +1016,20 @@ export function TranslationBatchFiltersSidebar() {
|
|
|
993
1016
|
})), 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) => ({
|
|
994
1017
|
...prev,
|
|
995
1018
|
status: value,
|
|
996
|
-
})), placeholder: "Select status" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "User" }), _jsx(UserPicker, { value:
|
|
1019
|
+
})), placeholder: "Select status" })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "User" }), _jsx(UserPicker, { value: userFilterLocked
|
|
997
1020
|
? currentUserName
|
|
998
1021
|
: filters.user === "all"
|
|
999
1022
|
? null
|
|
1000
|
-
: filters.user, displayValue:
|
|
1023
|
+
: filters.user, displayValue: userFilterLocked || filters.user === currentUserName
|
|
1001
1024
|
? `${editContext?.user?.fullName?.trim() || currentUserName} (You)`
|
|
1002
|
-
: undefined, displayAvatarName:
|
|
1025
|
+
: undefined, displayAvatarName: userFilterLocked || filters.user === currentUserName
|
|
1003
1026
|
? editContext?.user?.fullName?.trim() || currentUserName
|
|
1004
|
-
: undefined, displayAvatarUrl:
|
|
1027
|
+
: undefined, displayAvatarUrl: userFilterLocked || filters.user === currentUserName
|
|
1005
1028
|
? editContext?.user?.avatarUrl
|
|
1006
1029
|
: undefined, onChange: (user) => setTranslationBatchFilters((prev) => ({
|
|
1007
1030
|
...prev,
|
|
1008
1031
|
user: user?.userName ?? "all",
|
|
1009
|
-
})), disabled:
|
|
1032
|
+
})), disabled: userFilterLocked, 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: userFilterLocked || filters.user !== "all", searchUsers: searchTranslationUsers })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Provider" }), _jsx(Select, { className: "w-full bg-white", options: [
|
|
1010
1033
|
{ value: "all", label: "All Providers" },
|
|
1011
1034
|
...providers.map((p) => ({
|
|
1012
1035
|
value: p.name,
|
|
@@ -1206,22 +1229,66 @@ function computeBatchStatusCounts(info, jobs) {
|
|
|
1206
1229
|
const pending = info?.status === "Aborted" ? 0 : remaining;
|
|
1207
1230
|
return { total, succeeded, failed, aborted, running: 0, pending };
|
|
1208
1231
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1232
|
+
// One outcome per batch, shared by the ring, the bar and the status badge so
|
|
1233
|
+
// the three never disagree about what colour/state a batch is in.
|
|
1234
|
+
function resolveBatchOutcome(status, counts) {
|
|
1235
|
+
const { total, succeeded, failed } = counts;
|
|
1236
|
+
if (isActiveBatchStatus(status))
|
|
1237
|
+
return "active";
|
|
1238
|
+
if (total > 0 && succeeded === total)
|
|
1239
|
+
return "succeeded";
|
|
1240
|
+
if (total > 0 && failed === total)
|
|
1241
|
+
return "failed";
|
|
1242
|
+
if (status === "Aborted")
|
|
1243
|
+
return "aborted";
|
|
1244
|
+
return "mixed";
|
|
1245
|
+
}
|
|
1246
|
+
function BatchProgressIndicator({ counts, status, testId, variant = "circle", }) {
|
|
1247
|
+
const { total, succeeded, failed, aborted, running, pending } = counts;
|
|
1248
|
+
const outcome = resolveBatchOutcome(status, counts);
|
|
1249
|
+
const isActive = outcome === "active";
|
|
1250
|
+
const processed = succeeded + failed + aborted;
|
|
1251
|
+
// SVG filter ids are document-global; derive a safe one per instance.
|
|
1252
|
+
const glowId = `batch-glow-${React.useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
1253
|
+
// While the batch is running the number is "how far along are we", so it
|
|
1254
|
+
// counts every finished job. Once terminal it is the success rate.
|
|
1255
|
+
const percentBase = isActive ? processed : succeeded;
|
|
1211
1256
|
const percent = total > 0
|
|
1212
|
-
? Math.max(0, Math.min(100, Math.round((
|
|
1257
|
+
? Math.max(0, Math.min(100, Math.round((percentBase / total) * 100)))
|
|
1213
1258
|
: 0;
|
|
1214
|
-
|
|
1215
|
-
//
|
|
1259
|
+
// Colour-carrying segments in tooltip order; queued work stays in the
|
|
1260
|
+
// track. "Still running" is conveyed by the soft halo around the ring (and
|
|
1261
|
+
// bar) rather than by a segment, so the ring stays calm and readable.
|
|
1216
1262
|
const segments = [
|
|
1217
1263
|
{ key: "succeeded", count: succeeded, color: BATCH_SEGMENT_COLORS.succeeded },
|
|
1218
1264
|
{ key: "failed", count: failed, color: BATCH_SEGMENT_COLORS.failed },
|
|
1219
1265
|
{ key: "aborted", count: aborted, color: BATCH_SEGMENT_COLORS.aborted },
|
|
1220
1266
|
{ key: "running", count: running, color: BATCH_SEGMENT_COLORS.running },
|
|
1221
1267
|
].filter((seg) => seg.count > 0);
|
|
1222
|
-
//
|
|
1223
|
-
|
|
1224
|
-
|
|
1268
|
+
// Breathing halo shown only while the batch is active.
|
|
1269
|
+
const glowShadow = "0 0 6px 1px color-mix(in srgb, var(--color-feedback-green) 45%, transparent)";
|
|
1270
|
+
// The centre glyph/number follows the batch outcome so it never contradicts
|
|
1271
|
+
// the ring: green only when everything succeeded, red only when everything
|
|
1272
|
+
// failed, orange when aborted, neutral for running and mixed results.
|
|
1273
|
+
const centreFill = outcome === "succeeded"
|
|
1274
|
+
? "var(--color-feedback-green-light)"
|
|
1275
|
+
: outcome === "failed"
|
|
1276
|
+
? "var(--color-feedback-red-light)"
|
|
1277
|
+
: outcome === "aborted"
|
|
1278
|
+
? "var(--color-feedback-orange-light)"
|
|
1279
|
+
: null;
|
|
1280
|
+
const renderCentreGlyph = (sizeClass) => {
|
|
1281
|
+
if (outcome === "succeeded") {
|
|
1282
|
+
return (_jsx(CheckIcon, { className: `text-feedback-green ${sizeClass}`, style: { strokeWidth: 3 }, "aria-hidden": "true" }));
|
|
1283
|
+
}
|
|
1284
|
+
if (outcome === "failed") {
|
|
1285
|
+
return (_jsx(AlertCircleIcon, { className: `text-feedback-red ${sizeClass}`, style: { strokeWidth: 2.5 }, "aria-hidden": "true" }));
|
|
1286
|
+
}
|
|
1287
|
+
if (outcome === "aborted") {
|
|
1288
|
+
return (_jsx(AbortIcon, { className: `text-feedback-orange ${sizeClass}`, style: { strokeWidth: 2.5 }, "aria-hidden": "true" }));
|
|
1289
|
+
}
|
|
1290
|
+
return null;
|
|
1291
|
+
};
|
|
1225
1292
|
const parts = [`${succeeded}/${total} succeeded`];
|
|
1226
1293
|
if (failed > 0)
|
|
1227
1294
|
parts.push(`${failed} failed`);
|
|
@@ -1229,20 +1296,24 @@ function BatchProgressIndicator({ counts, testId, variant = "circle", }) {
|
|
|
1229
1296
|
parts.push(`${aborted} aborted`);
|
|
1230
1297
|
if (running > 0)
|
|
1231
1298
|
parts.push(`${running} running`);
|
|
1299
|
+
if (isActive && pending > 0)
|
|
1300
|
+
parts.push(`${pending} queued`);
|
|
1232
1301
|
const label = parts.join(", ");
|
|
1233
1302
|
if (variant === "bar") {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1303
|
+
const glyph = renderCentreGlyph("h-3 w-3 shrink-0");
|
|
1304
|
+
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, "data-outcome": outcome, children: [_jsxs("div", { className: "relative min-w-0 flex-1", children: [isActive && (_jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute -inset-px rounded-full animate-pulse", style: { boxShadow: glowShadow } })), _jsx("div", { className: "bg-neutral-grey-15 flex h-1.5 min-w-0 overflow-hidden rounded-full", children: total > 0 &&
|
|
1305
|
+
segments.map((seg) => (_jsx("div", { className: "h-full", style: {
|
|
1306
|
+
width: `${(seg.count / total) * 100}%`,
|
|
1307
|
+
backgroundColor: seg.color,
|
|
1308
|
+
} }, seg.key))) })] }), glyph ?? (_jsxs("span", { className: "shrink-0 text-[11px] leading-none font-medium tabular-nums text-neutral-grey-100", children: [percent, "%"] }))] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: label })] }));
|
|
1239
1309
|
}
|
|
1240
1310
|
const radius = 16;
|
|
1241
1311
|
const strokeWidth = 3;
|
|
1242
1312
|
const circumference = 2 * Math.PI * radius;
|
|
1243
1313
|
const gap = segments.length > 1 ? circumference * 0.02 : 0;
|
|
1244
1314
|
let segStart = 0;
|
|
1245
|
-
|
|
1315
|
+
const glyph = renderCentreGlyph("h-4 w-4");
|
|
1316
|
+
return (_jsxs(Tooltip, { delayDuration: 250, children: [_jsx(TooltipTrigger, { asChild: true, children: _jsxs("div", { className: "relative h-10 w-10 shrink-0 rounded-full", "aria-label": label, "data-testid": testId, "data-outcome": outcome, children: [_jsxs("svg", { className: "h-10 w-10 -rotate-90 overflow-visible", viewBox: "0 0 40 40", children: [isActive && (_jsxs(_Fragment, { children: [_jsx("defs", { children: _jsx("filter", { id: glowId, x: "-50%", y: "-50%", width: "200%", height: "200%", children: _jsx("feGaussianBlur", { stdDeviation: "2" }) }) }), _jsx("circle", { cx: "20", cy: "20", r: radius, fill: "none", stroke: "var(--color-feedback-green)", strokeWidth: strokeWidth + 4, filter: `url(#${glowId})`, opacity: 0.35, children: _jsx("animate", { attributeName: "opacity", values: "0.15;0.5;0.15", dur: "1.8s", repeatCount: "indefinite" }) })] })), centreFill && (_jsx("circle", { cx: "20", cy: "20", r: radius - strokeWidth / 2, fill: centreFill })), _jsx("circle", { cx: "20", cy: "20", r: radius, fill: "none", stroke: "var(--color-neutral-grey-15)", strokeWidth: strokeWidth }), total > 0 &&
|
|
1246
1317
|
segments.map((seg) => {
|
|
1247
1318
|
const segLen = (seg.count / total) * circumference;
|
|
1248
1319
|
const dash = Math.max(0, segLen - gap);
|
|
@@ -1252,7 +1323,7 @@ function BatchProgressIndicator({ counts, testId, variant = "circle", }) {
|
|
|
1252
1323
|
strokeDasharray: `${dash} ${circumference}`,
|
|
1253
1324
|
strokeDashoffset: dashOffset,
|
|
1254
1325
|
} }, seg.key));
|
|
1255
|
-
})] }), _jsx("span", { className: "absolute inset-0 flex items-center justify-center", children:
|
|
1326
|
+
})] }), _jsx("span", { className: "absolute inset-0 flex items-center justify-center", children: glyph ?? (_jsxs("span", { className: "text-[10px] leading-none font-medium tabular-nums text-neutral-grey-100", children: [percent, "%"] })) })] }) }), _jsx(TooltipContent, { side: "top", sideOffset: 6, children: label })] }));
|
|
1256
1327
|
}
|
|
1257
1328
|
function itemStatusDot(jobs) {
|
|
1258
1329
|
const hasError = jobs.some((j) => j.status === "Error");
|
|
@@ -1269,7 +1340,44 @@ function itemStatusDot(jobs) {
|
|
|
1269
1340
|
return { cls: "bg-feedback-orange", label: "Aborted" };
|
|
1270
1341
|
return { cls: "bg-feedback-green/70", label: "Completed" };
|
|
1271
1342
|
}
|
|
1272
|
-
|
|
1343
|
+
// Fallback one-row cap used until the first chip row has been measured.
|
|
1344
|
+
const COLLAPSED_CHIP_ROW_HEIGHT_PX = 28;
|
|
1345
|
+
function InlineLanguageJobChips({ batchId, batchProvider, jobs, languageMap, isExpanded, batchStartedAtUtc, abortingJobIds, retryingKeys, onToggle, onOpenItem, onAbortJob, onRetryJobs, }) {
|
|
1346
|
+
const chipAreaRef = React.useRef(null);
|
|
1347
|
+
const [hiddenCount, setHiddenCount] = React.useState(0);
|
|
1348
|
+
const [firstRowHeight, setFirstRowHeight] = React.useState(null);
|
|
1349
|
+
// Chips always wrap so they can never run into the neighbouring column.
|
|
1350
|
+
// When the item is collapsed only the first line is shown; count the chips
|
|
1351
|
+
// that wrapped onto later lines so a "+N" chip can stand in for them.
|
|
1352
|
+
React.useLayoutEffect(() => {
|
|
1353
|
+
const el = chipAreaRef.current;
|
|
1354
|
+
if (!el || isExpanded) {
|
|
1355
|
+
setHiddenCount(0);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
const measure = () => {
|
|
1359
|
+
const children = Array.from(el.children);
|
|
1360
|
+
if (children.length === 0) {
|
|
1361
|
+
setHiddenCount(0);
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
const firstTop = children[0]?.offsetTop ?? 0;
|
|
1365
|
+
let hidden = 0;
|
|
1366
|
+
for (const child of children) {
|
|
1367
|
+
if (child.offsetTop > firstTop)
|
|
1368
|
+
hidden += 1;
|
|
1369
|
+
}
|
|
1370
|
+
setHiddenCount(hidden);
|
|
1371
|
+
setFirstRowHeight(children[0]?.offsetHeight ?? null);
|
|
1372
|
+
};
|
|
1373
|
+
measure();
|
|
1374
|
+
let observer;
|
|
1375
|
+
if (typeof ResizeObserver !== "undefined") {
|
|
1376
|
+
observer = new ResizeObserver(measure);
|
|
1377
|
+
observer.observe(el);
|
|
1378
|
+
}
|
|
1379
|
+
return () => observer?.disconnect();
|
|
1380
|
+
}, [isExpanded, jobs.length]);
|
|
1273
1381
|
if (jobs.length === 0)
|
|
1274
1382
|
return null;
|
|
1275
1383
|
const handleCellClick = (event) => {
|
|
@@ -1283,15 +1391,20 @@ function InlineLanguageJobChips({ batchId, batchProvider, jobs, languageMap, isE
|
|
|
1283
1391
|
onToggle();
|
|
1284
1392
|
}
|
|
1285
1393
|
};
|
|
1286
|
-
return (
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
:
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1394
|
+
return (_jsxs("div", { role: "button", tabIndex: 0, className: "flex cursor-pointer items-start gap-1.5", "aria-label": `${jobs.length} target language${jobs.length !== 1 ? "s" : ""}`, "aria-expanded": isExpanded, onClick: handleCellClick, onKeyDown: handleCellKeyDown, children: [_jsx("div", { ref: chipAreaRef, className: `flex flex-1 flex-wrap items-start gap-1.5 ${isExpanded ? "" : "overflow-hidden"}`,
|
|
1395
|
+
// The cap is the measured height of one chip row, applied inline so
|
|
1396
|
+
// the clip does not depend on a Tailwind max-height utility being
|
|
1397
|
+
// emitted for this file.
|
|
1398
|
+
style: isExpanded
|
|
1399
|
+
? undefined
|
|
1400
|
+
: { maxHeight: firstRowHeight ?? COLLAPSED_CHIP_ROW_HEIGHT_PX }, children: jobs.map((job, idx) => {
|
|
1401
|
+
const lang = languageMap.get((job.targetLanguage || "").toLowerCase());
|
|
1402
|
+
const retryKey = `job:${job.id ?? `${job.itemId}:${job.sourceLanguage}:${job.targetLanguage}`}`;
|
|
1403
|
+
return (_jsx("span", { className: "shrink-0", onClick: (event) => event.stopPropagation(), children: _jsx(LanguageJobChip, { job: job, language: lang, provider: batchProvider, 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}`));
|
|
1404
|
+
}) }), !isExpanded && hiddenCount > 0 && (_jsxs("button", { type: "button", className: "mt-px shrink-0 rounded border border-border-default bg-neutral-grey-5 badge-pad-sm text-[12px] leading-none tabular-nums text-neutral-grey-50 transition-colors hover:bg-neutral-grey-15 hover:text-neutral-grey-100", "aria-label": `Show ${hiddenCount} more language${hiddenCount !== 1 ? "s" : ""}`, title: `${hiddenCount} more language${hiddenCount !== 1 ? "s" : ""}`, onClick: (event) => {
|
|
1405
|
+
event.stopPropagation();
|
|
1406
|
+
onToggle();
|
|
1407
|
+
}, children: ["+", hiddenCount] }))] }));
|
|
1295
1408
|
}
|
|
1296
1409
|
function CustomPromptPanel({ prompts }) {
|
|
1297
1410
|
const [openProvider, setOpenProvider] = React.useState(null);
|
|
@@ -1431,7 +1544,10 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
|
|
|
1431
1544
|
}
|
|
1432
1545
|
if (stackLanguageChips) {
|
|
1433
1546
|
const normalMinWidth = normalTableMinWidthRef.current;
|
|
1434
|
-
|
|
1547
|
+
// Unstack once the columns layout fits again. Before that layout has
|
|
1548
|
+
// ever been measured, fall back to the threshold that forced stacking
|
|
1549
|
+
// so a narrow-then-wide resize does not stay stacked forever.
|
|
1550
|
+
if (availableWidth >= (normalMinWidth ?? 640)) {
|
|
1435
1551
|
setStackLanguageChips(false);
|
|
1436
1552
|
}
|
|
1437
1553
|
return;
|
|
@@ -1476,7 +1592,7 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
|
|
|
1476
1592
|
byItem.set(key, []);
|
|
1477
1593
|
byItem.get(key).push(job);
|
|
1478
1594
|
}
|
|
1479
|
-
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) => {
|
|
1595
|
+
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: "w-12 pt-3 pr-3 pb-1 md:pr-4" }))] }) }), _jsx("tbody", { children: Array.from(byItem.entries()).map(([itemId, itemJobs], itemIndex) => {
|
|
1480
1596
|
const isItemExpanded = expandedItems.has(itemId);
|
|
1481
1597
|
const itemAnyInProgress = itemJobs.some((j) => j.status === "In Progress");
|
|
1482
1598
|
const itemAnyPending = itemJobs.some((j) => j.status === "Pending");
|
|
@@ -1496,7 +1612,7 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
|
|
|
1496
1612
|
.filter((language) => Boolean(language));
|
|
1497
1613
|
const normalizedCurrentLanguage = normalizeLanguageCode(currentLanguage);
|
|
1498
1614
|
const rowOpenLanguage = rowLanguages.find((language) => normalizeLanguageCode(language) === normalizedCurrentLanguage) ?? rowLanguages[0];
|
|
1499
|
-
const languageChips = (_jsx(InlineLanguageJobChips, { batchId: batchId, batchProvider: batchProvider, jobs: itemJobs, languageMap: languageMap, isExpanded: isItemExpanded, batchStartedAtUtc: batchStartedAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys,
|
|
1615
|
+
const languageChips = (_jsx(InlineLanguageJobChips, { batchId: batchId, batchProvider: batchProvider, jobs: itemJobs, languageMap: languageMap, isExpanded: isItemExpanded, batchStartedAtUtc: batchStartedAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys, onToggle: () => onToggleItem(itemId), onOpenItem: onOpenItem, onAbortJob: onAbortJob, onRetryJobs: onRetryJobs }));
|
|
1500
1616
|
const showRetryAction = itemErrorJobs.length > 0;
|
|
1501
1617
|
const retryAction = showRetryAction ? (_jsx("div", { className: "flex shrink-0 items-center justify-end", children: _jsx(SimpleIconButton, { onClick: (event) => {
|
|
1502
1618
|
event.stopPropagation();
|
|
@@ -1514,7 +1630,7 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
|
|
|
1514
1630
|
e.preventDefault();
|
|
1515
1631
|
void onOpenItem(itemId, rowOpenLanguage);
|
|
1516
1632
|
}
|
|
1517
|
-
}, 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));
|
|
1633
|
+
}, 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: "w-12 py-2 pr-3 align-top whitespace-nowrap md:pr-4", children: retryAction }))] }, itemId));
|
|
1518
1634
|
}) })] }) }));
|
|
1519
1635
|
}
|
|
1520
1636
|
function LanguageJobChip({ job, language, provider, batchStartedAtUtc, onOpen, isAborting = false, onAbort, isRetrying = false, onRetry, }) {
|
|
@@ -1607,10 +1723,10 @@ function LanguageJobChip({ job, language, provider, batchStartedAtUtc, onOpen, i
|
|
|
1607
1723
|
const emptyFieldText = statistics && statistics.emptyFieldCount > 0
|
|
1608
1724
|
? `${formatCount(statistics.emptyFieldCount)} empty`
|
|
1609
1725
|
: null;
|
|
1610
|
-
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, "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-[
|
|
1726
|
+
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, "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-[26rem] 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
|
|
1611
1727
|
? "Approximate — measured from batch start"
|
|
1612
1728
|
: 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 &&
|
|
1613
|
-
(jobError || jobAborted ? (_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: [canInspectRawResponse && (_jsx(ContentInspectorPopover, { title: "Raw AI response", emptyMessage: "Nothing captured for this job yet. Captures are kept in memory only and exist for AI provider jobs that ran since the last restart.", downloadFileName: `translation-job-${job.id}-raw-response.txt`, className: "w-[min(64rem,calc(100vw-2rem))]", side: "
|
|
1729
|
+
(jobError || jobAborted ? (_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 flex-wrap items-center justify-end gap-2 border-t border-border-default px-3 py-2", children: [canInspectRawResponse && (_jsx(ContentInspectorPopover, { title: "Raw AI response", emptyMessage: "Nothing captured for this job yet. Captures are kept in memory only and exist for AI provider jobs that ran since the last restart.", downloadFileName: `translation-job-${job.id}-raw-response.txt`, className: "w-[min(64rem,calc(100vw-2rem))]", side: "bottom", align: "end", loadSections: async () => {
|
|
1614
1730
|
const { payload, rawResponse } = await getTranslationJobLastPayload(job.id);
|
|
1615
1731
|
const sections = [];
|
|
1616
1732
|
if (rawResponse) {
|
|
@@ -1620,14 +1736,17 @@ function LanguageJobChip({ job, language, provider, batchStartedAtUtc, onOpen, i
|
|
|
1620
1736
|
});
|
|
1621
1737
|
}
|
|
1622
1738
|
if (payload) {
|
|
1623
|
-
sections.push({
|
|
1739
|
+
sections.push({
|
|
1740
|
+
title: "Last request payload",
|
|
1741
|
+
content: payload,
|
|
1742
|
+
});
|
|
1624
1743
|
}
|
|
1625
1744
|
return sections;
|
|
1626
1745
|
}, trigger: _jsxs(Button, { size: "sm", variant: "outline", "data-testid": "translation-job-raw-response-button", onClick: (e) => e.stopPropagation(), className: "mr-auto gap-1.5 text-neutral-grey-50", title: "Show the raw AI request and responses for this job", children: [_jsx(BugIcon, { className: "h-3.5 w-3.5" }), "Raw response"] }) })), canAbort && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isAborting, onClick: (e) => {
|
|
1627
1746
|
e.stopPropagation();
|
|
1628
1747
|
void onAbort?.();
|
|
1629
1748
|
setOpen(false);
|
|
1630
|
-
}, className: "gap-1.5 text-
|
|
1749
|
+
}, className: "gap-1.5 text-neutral-grey-50 hover:text-neutral-grey-100", children: [isAborting ? (_jsx(LoaderIcon, { className: "h-3.5 w-3.5 animate-spin" })) : (_jsx(AbortIcon, { className: "h-3.5 w-3.5" })), "Abort"] })), jobError && onRetry && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isRetrying, onClick: (e) => {
|
|
1631
1750
|
e.stopPropagation();
|
|
1632
1751
|
void onRetry();
|
|
1633
1752
|
setOpen(false);
|