@parhelia/localization 0.4.12800 → 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 +44 -21
- 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;
|
|
@@ -906,12 +922,16 @@ export function TranslationBatchFiltersSidebar() {
|
|
|
906
922
|
const [sitecoreLanguages, setSitecoreLanguages] = useState([]);
|
|
907
923
|
const [itemPickerOpen, setItemPickerOpen] = useState(false);
|
|
908
924
|
const currentUserName = editContext?.user?.name || "all";
|
|
909
|
-
|
|
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;
|
|
910
930
|
useEffect(() => {
|
|
911
931
|
ensureDefaultTranslationBatchUser(currentUserName);
|
|
912
932
|
}, [currentUserName]);
|
|
913
933
|
useEffect(() => {
|
|
914
|
-
if (
|
|
934
|
+
if (userFilterLocked &&
|
|
915
935
|
currentUserName !== "all" &&
|
|
916
936
|
filters.user !== currentUserName) {
|
|
917
937
|
setTranslationBatchFilters((prev) => ({
|
|
@@ -919,7 +939,7 @@ export function TranslationBatchFiltersSidebar() {
|
|
|
919
939
|
user: currentUserName,
|
|
920
940
|
}));
|
|
921
941
|
}
|
|
922
|
-
}, [
|
|
942
|
+
}, [userFilterLocked, currentUserName, filters.user]);
|
|
923
943
|
const searchTranslationUsers = useCallback((query, limit) => searchBackendUsers(query, limit), []);
|
|
924
944
|
const debouncedSetItemFilter = useDebouncedCallback((value) => setTranslationBatchFilters((prev) => ({
|
|
925
945
|
...prev,
|
|
@@ -996,20 +1016,20 @@ export function TranslationBatchFiltersSidebar() {
|
|
|
996
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) => ({
|
|
997
1017
|
...prev,
|
|
998
1018
|
status: value,
|
|
999
|
-
})), 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
|
|
1000
1020
|
? currentUserName
|
|
1001
1021
|
: filters.user === "all"
|
|
1002
1022
|
? null
|
|
1003
|
-
: filters.user, displayValue:
|
|
1023
|
+
: filters.user, displayValue: userFilterLocked || filters.user === currentUserName
|
|
1004
1024
|
? `${editContext?.user?.fullName?.trim() || currentUserName} (You)`
|
|
1005
|
-
: undefined, displayAvatarName:
|
|
1025
|
+
: undefined, displayAvatarName: userFilterLocked || filters.user === currentUserName
|
|
1006
1026
|
? editContext?.user?.fullName?.trim() || currentUserName
|
|
1007
|
-
: undefined, displayAvatarUrl:
|
|
1027
|
+
: undefined, displayAvatarUrl: userFilterLocked || filters.user === currentUserName
|
|
1008
1028
|
? editContext?.user?.avatarUrl
|
|
1009
1029
|
: undefined, onChange: (user) => setTranslationBatchFilters((prev) => ({
|
|
1010
1030
|
...prev,
|
|
1011
1031
|
user: user?.userName ?? "all",
|
|
1012
|
-
})), 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: [
|
|
1013
1033
|
{ value: "all", label: "All Providers" },
|
|
1014
1034
|
...providers.map((p) => ({
|
|
1015
1035
|
value: p.name,
|
|
@@ -1703,10 +1723,10 @@ function LanguageJobChip({ job, language, provider, batchStartedAtUtc, onOpen, i
|
|
|
1703
1723
|
const emptyFieldText = statistics && statistics.emptyFieldCount > 0
|
|
1704
1724
|
? `${formatCount(statistics.emptyFieldCount)} empty`
|
|
1705
1725
|
: null;
|
|
1706
|
-
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
|
|
1707
1727
|
? "Approximate — measured from batch start"
|
|
1708
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 &&
|
|
1709
|
-
(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 () => {
|
|
1710
1730
|
const { payload, rawResponse } = await getTranslationJobLastPayload(job.id);
|
|
1711
1731
|
const sections = [];
|
|
1712
1732
|
if (rawResponse) {
|
|
@@ -1716,14 +1736,17 @@ function LanguageJobChip({ job, language, provider, batchStartedAtUtc, onOpen, i
|
|
|
1716
1736
|
});
|
|
1717
1737
|
}
|
|
1718
1738
|
if (payload) {
|
|
1719
|
-
sections.push({
|
|
1739
|
+
sections.push({
|
|
1740
|
+
title: "Last request payload",
|
|
1741
|
+
content: payload,
|
|
1742
|
+
});
|
|
1720
1743
|
}
|
|
1721
1744
|
return sections;
|
|
1722
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) => {
|
|
1723
1746
|
e.stopPropagation();
|
|
1724
1747
|
void onAbort?.();
|
|
1725
1748
|
setOpen(false);
|
|
1726
|
-
}, 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) => {
|
|
1727
1750
|
e.stopPropagation();
|
|
1728
1751
|
void onRetry();
|
|
1729
1752
|
setOpen(false);
|