@parhelia/localization 0.1.12912 → 0.1.12917

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"TranslationBatches.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationBatches.tsx"],"names":[],"mappings":"AA6UA,wBAAgB,kBAAkB,4CAmlCjC;AAED,eAAO,MAAM,oCAAoC,8BAA8B,CAAC;AAEhF,wBAAgB,8BAA8B,4CA0Y7C"}
1
+ {"version":3,"file":"TranslationBatches.d.ts","sourceRoot":"","sources":["../../src/translation-center/TranslationBatches.tsx"],"names":[],"mappings":"AA4UA,wBAAgB,kBAAkB,4CAklCjC;AAED,eAAO,MAAM,oCAAoC,8BAA8B,CAAC;AAEhF,wBAAgB,8BAA8B,4CA0Y7C"}
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useRef, useState, useMemo, useSyncExternalStore, } from "react";
3
3
  import React from "react";
4
- import { Button, Badge, Select, Input, UserPicker, Popover, PopoverContent, PopoverTrigger, Tooltip, TooltipContent, TooltipTrigger, ContentTree, PageHeader, fetchItemStubs, searchUsers as searchBackendUsers, useEditContext, getLanguages, SimpleIconButton, DeleteIcon, formatDateTime, } from "@parhelia/core";
5
- import { listBatches, getTranslationProviders, listBatchTranslationJobs, retryBatchTranslation, abortBatch, deleteBatch, } from "../services/translationService";
4
+ import { Button, Badge, Select, Input, UserPicker, Popover, PopoverContent, PopoverTrigger, Tooltip, TooltipContent, TooltipTrigger, ContentTree, PageHeader, fetchItemStubs, searchUsers as searchBackendUsers, useEditContext, getLanguages, SimpleIconButton, formatDateTime, } from "@parhelia/core";
5
+ import { listBatches, getTranslationProviders, listBatchTranslationJobs, retryBatchTranslation, abortJob, abortBatch, } from "../services/translationService";
6
6
  import { TRANSLATION_BATCH_STARTED_EVENT, } from "../translationEvents";
7
7
  import { useDebouncedCallback } from "use-debounce";
8
8
  import { X, ChevronDown, Languages, Loader2, Globe, FolderTree, ExternalLink, Check, AlertCircle, CircleStop, Hourglass, Copy, Sparkles, RotateCcw, Plus, } from "lucide-react";
@@ -187,7 +187,7 @@ export function TranslationBatches() {
187
187
  const [batchJobs, setBatchJobs] = useState({});
188
188
  const [loadingJobs, setLoadingJobs] = useState(new Set());
189
189
  const [abortingBatchIds, setAbortingBatchIds] = useState(new Set());
190
- const [deletingBatchIds, setDeletingBatchIds] = useState(new Set());
190
+ const [abortingJobIds, setAbortingJobIds] = useState(new Set());
191
191
  const [retryingKeys, setRetryingKeys] = useState(new Set());
192
192
  // Cache of userName → displayName so we can render full names on batch rows
193
193
  // without re-querying the user service every render.
@@ -603,41 +603,54 @@ export function TranslationBatches() {
603
603
  loadBatchJobs,
604
604
  loadRecentBatches,
605
605
  ]);
606
- const handleDeleteBatch = useCallback((batchId) => {
606
+ const handleAbortJob = useCallback((job) => {
607
+ if (!job.id || !job.batchId) {
608
+ toast.error("This translation job cannot be aborted.");
609
+ return;
610
+ }
611
+ const jobId = job.id;
612
+ const batchId = job.batchId;
613
+ const targetLabel = job.targetLanguage
614
+ ? `${job.targetLanguage} translation`
615
+ : "translation";
607
616
  editContext?.confirm({
608
- header: "Delete Translation Batch from History",
609
- message: "Delete this translation batch from history? This removes only translation job records; it does not delete Sitecore items or translated content.",
610
- acceptLabel: "Delete",
617
+ header: "Abort Translation Job",
618
+ message: `Abort this ${targetLabel} job? This stops the remaining translation work for this language, but it does not roll back content that was already written.`,
619
+ acceptLabel: "Abort",
611
620
  showCancel: true,
612
621
  accept: async () => {
613
- setDeletingBatchIds((prev) => new Set(prev).add(batchId));
622
+ setAbortingJobIds((prev) => new Set(prev).add(jobId));
614
623
  try {
615
- const result = await deleteBatch(batchId);
624
+ const result = await abortJob(jobId);
616
625
  if (result.type !== "success") {
617
626
  toast.error(result.details ||
618
627
  result.summary ||
619
- "Failed to delete translation batch.");
628
+ "Failed to abort translation job.");
620
629
  return;
621
630
  }
622
- toast.success("Translation batch deleted.");
623
- setBatches((prev) => prev.filter((batch) => batch.id !== batchId));
624
- setBatchJobs((prev) => {
625
- const next = { ...prev };
626
- delete next[batchId];
627
- return next;
628
- });
629
- setExpandedBatchId((prev) => (prev === batchId ? null : prev));
631
+ toast.success("Translation job aborted.");
632
+ await loadRecentBatches(0, false, effectiveFilters);
633
+ if (expandedBatchId === batchId || batchJobs[batchId]) {
634
+ await loadBatchJobs(batchId);
635
+ }
630
636
  }
631
637
  finally {
632
- setDeletingBatchIds((prev) => {
638
+ setAbortingJobIds((prev) => {
633
639
  const next = new Set(prev);
634
- next.delete(batchId);
640
+ next.delete(jobId);
635
641
  return next;
636
642
  });
637
643
  }
638
644
  },
639
645
  });
640
- }, [editContext]);
646
+ }, [
647
+ batchJobs,
648
+ editContext,
649
+ effectiveFilters,
650
+ expandedBatchId,
651
+ loadBatchJobs,
652
+ loadRecentBatches,
653
+ ]);
641
654
  const handleRetryJobs = useCallback(async (batchId, provider, jobs, retryKey) => {
642
655
  const sessionId = editContext?.sessionId;
643
656
  if (!sessionId) {
@@ -806,11 +819,9 @@ export function TranslationBatches() {
806
819
  const isLoadingThis = loadingJobs.has(b.id);
807
820
  const canRetry = anyError;
808
821
  const canAbort = isActiveBatchStatus(info?.status);
809
- const canDelete = isTerminalBatchStatus(info?.status);
810
822
  const isRetryingBatch = retryingKeys.has(`batch:${b.id}`);
811
823
  const isAborting = abortingBatchIds.has(b.id);
812
- const isDeleting = deletingBatchIds.has(b.id);
813
- const hasBatchActions = canRetry || canAbort || canDelete;
824
+ const hasBatchActions = canRetry || canAbort;
814
825
  // Stable test-id status used by Playwright assertions.
815
826
  // Matches the visual status dot/spinner logic above.
816
827
  const testStatus = anyError
@@ -828,18 +839,12 @@ export function TranslationBatches() {
828
839
  }, icon: _jsx(RetryIcon, { className: `h-3.5 w-3.5 ${isRetryingBatch ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Retry failed translations", disabled: !canRetry ||
829
840
  isRetryingBatch ||
830
841
  isLoadingThis ||
831
- isAborting ||
832
- isDeleting })), canAbort && (_jsx(SimpleIconButton, { onClick: (event) => {
842
+ isAborting })), canAbort && (_jsx(SimpleIconButton, { onClick: (event) => {
833
843
  event.stopPropagation();
834
844
  if (!canAbort)
835
845
  return;
836
846
  handleAbortBatch(b.id);
837
- }, icon: _jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Abort translation batch", disabled: !canAbort || isAborting || isDeleting })), canDelete && (_jsx(SimpleIconButton, { onClick: (event) => {
838
- event.stopPropagation();
839
- if (!canDelete)
840
- return;
841
- handleDeleteBatch(b.id);
842
- }, icon: _jsx(DeleteIcon, { size: "md" }), label: "Delete translation batch from history", disabled: !canDelete || isDeleting || isAborting }))] }));
847
+ }, icon: _jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}`, strokeWidth: 1.5 }), label: "Abort translation batch", disabled: !canAbort || isAborting }))] }));
843
848
  return (_jsxs("div", { "data-testid": `batch-row-${b.id}`, "data-batch-status": testStatus, children: [_jsx("div", { role: "button", tabIndex: 0, className: `group w-full cursor-pointer py-3 text-left transition-colors hover:bg-neutral-grey-5 ${isCompactBatches ? "px-2.5" : "px-4"}`, onClick: () => toggleBatch(b.id), onKeyDown: (e) => {
844
849
  if (e.key === "Enter" || e.key === " ") {
845
850
  e.preventDefault();
@@ -851,7 +856,7 @@ export function TranslationBatches() {
851
856
  ? "whitespace-normal break-words leading-5"
852
857
  : "truncate"}`, style: isCompactBatches
853
858
  ? { overflowWrap: "anywhere" }
854
- : undefined, title: formattedBatchDate, children: batchDisplayName }), _jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: desktopMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, i))) }), mobileFooterMetaParts.length > 0 && (_jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: mobileFooterMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, `footer-${i}`))) }))] }), isCompactBatches && (_jsxs("div", { className: "mt-2 flex min-w-0 items-center gap-2", children: [totalJobs > 0 && (_jsx("div", { className: "min-w-0 flex-1", children: _jsx(BatchProgressIndicator, { value: progressPct, completedJobs: completedJobs, totalJobs: totalJobs, errorJobs: errorJobs, tone: progressTone, testId: `batch-progress-${b.id}`, variant: "bar" }) })), hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: batchActions }))] })), !isCompactBatches && hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-2 self-center", children: batchActions }))] })] }) }), isExpanded && (_jsxs("div", { className: "border-t border-border-default/50", children: [_jsx(CustomPromptPanel, { prompts: parseCustomPrompts(info?.metadata) }), _jsx(BatchItemList, { batchId: b.id, batchProvider: info?.provider, jobs: jobs, isLoading: isLoadingThis, expandedItems: expandedItems, languages: sitecoreLanguages, currentLanguage: editContext?.item?.language, itemFilter: filters.itemIdOrPath.trim(), itemIncludeSubitems: filters.itemIncludeSubitems, statusFilter: filters.status, batchIsTerminal: isTerminalBatchStatus(info?.status), batchStartedAtUtc: info?.startedAtUtc ?? info?.createdAtUtc, retryingKeys: retryingKeys, isMobile: isMobile, onToggleItem: toggleItem, onOpenItem: openItemInEditor, onRetryJobs: handleRetryJobs })] }))] }, b.id));
859
+ : undefined, title: formattedBatchDate, children: batchDisplayName }), _jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: desktopMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, i))) }), mobileFooterMetaParts.length > 0 && (_jsx("div", { className: "mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[12px] text-neutral-grey-50", children: mobileFooterMetaParts.map((part, i) => (_jsxs(React.Fragment, { children: [i > 0 && (_jsx("span", { className: "text-neutral-grey-15", children: "\u00B7" })), part] }, `footer-${i}`))) }))] }), isCompactBatches && (_jsxs("div", { className: "mt-2 flex min-w-0 items-center gap-2", children: [totalJobs > 0 && (_jsx("div", { className: "min-w-0 flex-1", children: _jsx(BatchProgressIndicator, { value: progressPct, completedJobs: completedJobs, totalJobs: totalJobs, errorJobs: errorJobs, tone: progressTone, testId: `batch-progress-${b.id}`, variant: "bar" }) })), hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: batchActions }))] })), !isCompactBatches && hasBatchActions && (_jsx("div", { className: "flex shrink-0 items-center gap-2 self-center", children: batchActions }))] })] }) }), isExpanded && (_jsxs("div", { className: "border-t border-border-default/50", children: [_jsx(CustomPromptPanel, { prompts: parseCustomPrompts(info?.metadata) }), _jsx(BatchItemList, { batchId: b.id, batchProvider: info?.provider, jobs: jobs, isLoading: isLoadingThis, expandedItems: expandedItems, languages: sitecoreLanguages, currentLanguage: editContext?.item?.language, itemFilter: filters.itemIdOrPath.trim(), itemIncludeSubitems: filters.itemIncludeSubitems, statusFilter: filters.status, batchIsTerminal: isTerminalBatchStatus(info?.status), batchStartedAtUtc: info?.startedAtUtc ?? info?.createdAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys, isMobile: isMobile, onToggleItem: toggleItem, onOpenItem: openItemInEditor, onAbortJob: handleAbortJob, onRetryJobs: handleRetryJobs })] }))] }, b.id));
855
860
  }) })] }, dateGroup.label))), hasMore && (_jsx("div", { className: "flex justify-center pt-6", children: _jsx(Button, { variant: "outline", size: "sm", onClick: loadMore, disabled: isLoadingMore, children: isLoadingMore ? (_jsxs(_Fragment, { children: [_jsx(LoaderIcon, { className: "h-4 w-4 animate-spin text-highlight-100" }), "Loading more..."] })) : (_jsxs(_Fragment, { children: [_jsx(ChevronDownIcon, { className: "h-4 w-4" }), "Load More Batches"] })) }) }))] })) }) })] }));
856
861
  }
857
862
  export const TRANSLATION_BATCH_FILTERS_SIDEBAR_ID = "translation-batch-filters";
@@ -965,7 +970,7 @@ export function TranslationBatchFiltersSidebar() {
965
970
  : undefined, onChange: (user) => setTranslationBatchFilters((prev) => ({
966
971
  ...prev,
967
972
  user: user?.userName ?? "all",
968
- })), disabled: isLimitedPreviewUser, placeholder: "All Users", buttonClassName: "h-auto min-h-[34px] w-full justify-start rounded-md border bg-white py-2 text-xs font-medium", allowClear: true, clearLabel: "All Users", showIcon: isLimitedPreviewUser || filters.user !== "all", searchUsers: searchTranslationUsers })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Provider" }), _jsx(Select, { className: "w-full bg-white", options: [
973
+ })), disabled: isLimitedPreviewUser, placeholder: "All Users", buttonClassName: "h-8 min-h-0 w-full justify-start rounded-[4px] border bg-white px-2 py-0 text-xs font-medium", allowClear: true, clearLabel: "All Users", showIcon: isLimitedPreviewUser || filters.user !== "all", searchUsers: searchTranslationUsers })] }), _jsxs("div", { className: filterFieldClass, children: [_jsx("label", { className: filterLabelClass, children: "Provider" }), _jsx(Select, { className: "w-full bg-white", options: [
969
974
  { value: "all", label: "All Providers" },
970
975
  ...providers.map((p) => ({
971
976
  value: p.name,
@@ -1170,7 +1175,7 @@ function itemStatusDot(jobs) {
1170
1175
  return { cls: "bg-feedback-orange", label: "Aborted" };
1171
1176
  return { cls: "bg-feedback-green/70", label: "Completed" };
1172
1177
  }
1173
- function InlineLanguageJobChips({ batchId, batchProvider, jobs, languageMap, isExpanded, batchStartedAtUtc, retryingKeys, singleLine = false, onToggle, onOpenItem, onRetryJobs, }) {
1178
+ function InlineLanguageJobChips({ batchId, batchProvider, jobs, languageMap, isExpanded, batchStartedAtUtc, abortingJobIds, retryingKeys, singleLine = false, onToggle, onOpenItem, onAbortJob, onRetryJobs, }) {
1174
1179
  if (jobs.length === 0)
1175
1180
  return null;
1176
1181
  const handleCellClick = (event) => {
@@ -1191,7 +1196,7 @@ function InlineLanguageJobChips({ batchId, batchProvider, jobs, languageMap, isE
1191
1196
  : "max-h-7 flex-wrap overflow-hidden"}`, "aria-label": `${jobs.length} target language${jobs.length !== 1 ? "s" : ""}`, "aria-expanded": isExpanded, onClick: handleCellClick, onKeyDown: handleCellKeyDown, children: jobs.map((job, idx) => {
1192
1197
  const lang = languageMap.get((job.targetLanguage || "").toLowerCase());
1193
1198
  const retryKey = `job:${job.id ?? `${job.itemId}:${job.sourceLanguage}:${job.targetLanguage}`}`;
1194
- return (_jsx("span", { className: "shrink-0", onClick: (event) => event.stopPropagation(), children: _jsx(LanguageJobChip, { job: job, language: lang, batchStartedAtUtc: batchStartedAtUtc, onOpen: () => onOpenItem(job.itemId, job.targetLanguage), isRetrying: retryingKeys.has(retryKey), onRetry: () => onRetryJobs(batchId, batchProvider, [job], retryKey) }) }, `${job.id ?? idx}-${job.targetLanguage}`));
1199
+ return (_jsx("span", { className: "shrink-0", onClick: (event) => event.stopPropagation(), children: _jsx(LanguageJobChip, { job: job, language: lang, batchStartedAtUtc: batchStartedAtUtc, onOpen: () => onOpenItem(job.itemId, job.targetLanguage), isAborting: !!job.id && abortingJobIds.has(job.id), onAbort: () => onAbortJob(job), isRetrying: retryingKeys.has(retryKey), onRetry: () => onRetryJobs(batchId, batchProvider, [job], retryKey) }) }, `${job.id ?? idx}-${job.targetLanguage}`));
1195
1200
  }) }));
1196
1201
  }
1197
1202
  function CustomPromptPanel({ prompts }) {
@@ -1232,7 +1237,7 @@ function CustomPromptPanel({ prompts }) {
1232
1237
  }, icon: justCopied ? (_jsx(CheckIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 })) : (_jsx(CopyIcon, { className: "h-3.5 w-3.5", strokeWidth: 1.5 })), label: justCopied ? "Copied" : "Copy prompt" })), _jsx(ChevronDownIcon, { className: `h-3.5 w-3.5 text-neutral-grey-100 transition-transform ${isOpen ? "rotate-180" : ""}` })] })] }), isOpen && (_jsx("div", { style: { paddingLeft: "2.5rem" }, className: "pr-3 md:pr-4 pb-3", children: _jsx("pre", { className: "max-h-64 overflow-y-auto whitespace-pre-wrap break-words rounded border border-border-default/50 bg-white p-2 font-mono text-[12px] text-neutral-grey-100", children: p.prompt }) }))] }, p.provider));
1233
1238
  }) }));
1234
1239
  }
1235
- function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems, languages, currentLanguage, itemFilter, itemIncludeSubitems, statusFilter, batchIsTerminal, batchStartedAtUtc, retryingKeys, isMobile = false, onToggleItem, onOpenItem, onRetryJobs, }) {
1240
+ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems, languages, currentLanguage, itemFilter, itemIncludeSubitems, statusFilter, batchIsTerminal, batchStartedAtUtc, abortingJobIds, retryingKeys, isMobile = false, onToggleItem, onOpenItem, onAbortJob, onRetryJobs, }) {
1236
1241
  const languageMap = React.useMemo(() => {
1237
1242
  const map = new Map();
1238
1243
  for (const l of languages) {
@@ -1359,11 +1364,7 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
1359
1364
  observer?.disconnect();
1360
1365
  window.removeEventListener("resize", updateLayout);
1361
1366
  };
1362
- }, [
1363
- isMobile,
1364
- stackLanguageChips,
1365
- statusFilteredJobs,
1366
- ]);
1367
+ }, [isMobile, stackLanguageChips, statusFilteredJobs]);
1367
1368
  if (isLoading && !jobs) {
1368
1369
  return (_jsxs("div", { className: "px-3 md:px-4 py-4 pl-8 text-[12px] text-neutral-grey-50", children: [_jsx(LoaderIcon, { className: "inline h-3.5 w-3.5 mr-1.5 animate-spin text-highlight-100" }), "Loading items\u2026"] }));
1369
1370
  }
@@ -1401,7 +1402,7 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
1401
1402
  .filter((language) => Boolean(language));
1402
1403
  const normalizedCurrentLanguage = normalizeLanguageCode(currentLanguage);
1403
1404
  const rowOpenLanguage = rowLanguages.find((language) => normalizeLanguageCode(language) === normalizedCurrentLanguage) ?? rowLanguages[0];
1404
- const languageChips = (_jsx(InlineLanguageJobChips, { batchId: batchId, batchProvider: batchProvider, jobs: itemJobs, languageMap: languageMap, isExpanded: isItemExpanded, batchStartedAtUtc: batchStartedAtUtc, retryingKeys: retryingKeys, singleLine: !stackLanguageChips, onToggle: () => onToggleItem(itemId), onOpenItem: onOpenItem, onRetryJobs: onRetryJobs }));
1405
+ const languageChips = (_jsx(InlineLanguageJobChips, { batchId: batchId, batchProvider: batchProvider, jobs: itemJobs, languageMap: languageMap, isExpanded: isItemExpanded, batchStartedAtUtc: batchStartedAtUtc, abortingJobIds: abortingJobIds, retryingKeys: retryingKeys, singleLine: !stackLanguageChips, onToggle: () => onToggleItem(itemId), onOpenItem: onOpenItem, onAbortJob: onAbortJob, onRetryJobs: onRetryJobs }));
1405
1406
  const showRetryAction = itemErrorJobs.length > 0;
1406
1407
  const retryAction = showRetryAction ? (_jsx("div", { className: "flex shrink-0 items-center justify-end", children: _jsx(SimpleIconButton, { onClick: (event) => {
1407
1408
  event.stopPropagation();
@@ -1422,13 +1423,14 @@ function BatchItemList({ batchId, batchProvider, jobs, isLoading, expandedItems,
1422
1423
  }, children: [!stackLanguageChips && (_jsx("td", { "aria-hidden": "true", className: "px-3 py-2 md:px-4" })), _jsx("td", { className: `py-2 align-top ${stackLanguageChips ? "pr-2 pl-2.5" : "pr-4"}`, children: itemContent }), !stackLanguageChips && (_jsx("td", { className: "py-2 pr-2 align-top", children: languageChips })), !stackLanguageChips && (_jsx("td", { className: "py-2 pr-3 align-top md:pr-4", children: retryAction }))] }, itemId));
1423
1424
  }) })] }) }));
1424
1425
  }
1425
- function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying = false, onRetry, }) {
1426
+ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isAborting = false, onAbort, isRetrying = false, onRetry, }) {
1426
1427
  const [open, setOpen] = React.useState(false);
1427
1428
  const jobStatus = job.status;
1428
1429
  const jobInProgress = jobStatus === "In Progress";
1429
1430
  const jobPending = jobStatus === "Pending";
1430
1431
  const jobError = jobStatus === "Error";
1431
1432
  const jobAborted = jobStatus === "Aborted";
1433
+ const canAbort = !!job.id && !!job.batchId && !!onAbort && (jobInProgress || jobPending);
1432
1434
  const langName = language?.name || job.targetLanguage;
1433
1435
  const chipCls = jobError
1434
1436
  ? "border-feedback-red bg-feedback-red-light text-feedback-red hover:bg-feedback-red-light"
@@ -1513,7 +1515,11 @@ function LanguageJobChip({ job, language, batchStartedAtUtc, onOpen, isRetrying
1513
1515
  // ITranslationService stack trace). Render them in a bounded,
1514
1516
  // scrollable monospace block so the popover stays a sensible size and
1515
1517
  // the original formatting is preserved.
1516
- _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: [jobError && onRetry && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isRetrying, onClick: (e) => {
1518
+ _jsxs("div", { className: "flex flex-col gap-1 px-3 py-2", children: [_jsx("dt", { className: "text-[10px] tracking-wide text-neutral-grey-50", children: jobError ? "Error" : "Aborted" }), _jsx("dd", { className: `max-h-40 overflow-auto whitespace-pre-wrap break-words rounded-sm bg-neutral-grey-5 p-2 font-mono text-[11px] leading-snug ${jobError ? "text-feedback-red" : "text-feedback-orange"}`, children: job.message })] })) : (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Progress" }), _jsx("dd", { className: "text-[12px] break-words text-neutral-grey-100", children: job.message })] }))), job.batchId && (_jsxs("div", { className: "flex items-baseline gap-3 px-3 py-2", children: [_jsx("dt", { className: "w-20 shrink-0 text-[10px] tracking-wide text-neutral-grey-50", children: "Batch" }), _jsx("dd", { className: "text-[11px] font-mono text-neutral-grey-50 break-all", children: job.batchId })] }))] }), _jsxs("div", { className: "flex justify-end gap-2 border-t border-border-default px-3 py-2", children: [canAbort && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isAborting, onClick: (e) => {
1519
+ e.stopPropagation();
1520
+ void onAbort?.();
1521
+ setOpen(false);
1522
+ }, className: "gap-1.5 text-feedback-orange hover:text-feedback-orange", children: [_jsx(AbortIcon, { className: `h-3.5 w-3.5 ${isAborting ? "animate-spin" : ""}` }), "Abort"] })), jobError && onRetry && (_jsxs(Button, { size: "sm", variant: "outline", disabled: isRetrying, onClick: (e) => {
1517
1523
  e.stopPropagation();
1518
1524
  void onRetry();
1519
1525
  setOpen(false);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parhelia/localization",
3
- "version": "0.1.12912",
3
+ "version": "0.1.12917",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"