@purposeinplay/payload-ai-translate 0.1.0
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/LICENSE +21 -0
- package/README.md +714 -0
- package/dist/alerts-collection.d.ts +21 -0
- package/dist/alerts-collection.js +159 -0
- package/dist/api.d.ts +4 -0
- package/dist/api.js +918 -0
- package/dist/bulk-translate-batches-collection.d.ts +29 -0
- package/dist/bulk-translate-batches-collection.js +404 -0
- package/dist/bulk-translate-units-collection.d.ts +35 -0
- package/dist/bulk-translate-units-collection.js +310 -0
- package/dist/client/estimated-cost-cell.d.ts +6 -0
- package/dist/client/estimated-cost-cell.js +12 -0
- package/dist/client/excluded-fields-field.d.ts +45 -0
- package/dist/client/excluded-fields-field.js +553 -0
- package/dist/client/field-translate-button.d.ts +6 -0
- package/dist/client/field-translate-button.js +199 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +6 -0
- package/dist/client/lib/use-global-kill-switches.d.ts +20 -0
- package/dist/client/lib/use-global-kill-switches.js +58 -0
- package/dist/client/translate-button.d.ts +2 -0
- package/dist/client/translate-button.js +228 -0
- package/dist/client/translate-modal.d.ts +16 -0
- package/dist/client/translate-modal.js +549 -0
- package/dist/client/translation-progress.d.ts +10 -0
- package/dist/client/translation-progress.js +297 -0
- package/dist/components/TranslationNavGroup.d.ts +45 -0
- package/dist/components/TranslationNavGroup.js +104 -0
- package/dist/defaults.d.ts +11 -0
- package/dist/defaults.js +16 -0
- package/dist/endpoints/client-config.d.ts +44 -0
- package/dist/endpoints/client-config.js +145 -0
- package/dist/endpoints/estimate.d.ts +5 -0
- package/dist/endpoints/estimate.js +237 -0
- package/dist/endpoints/progress.d.ts +2 -0
- package/dist/endpoints/progress.js +314 -0
- package/dist/endpoints/translate.d.ts +11 -0
- package/dist/endpoints/translate.js +376 -0
- package/dist/endpoints/translation-hub/_helpers.d.ts +140 -0
- package/dist/endpoints/translation-hub/_helpers.js +297 -0
- package/dist/endpoints/translation-hub/active.d.ts +21 -0
- package/dist/endpoints/translation-hub/active.js +220 -0
- package/dist/endpoints/translation-hub/cancel.d.ts +22 -0
- package/dist/endpoints/translation-hub/cancel.js +233 -0
- package/dist/endpoints/translation-hub/enqueue.d.ts +70 -0
- package/dist/endpoints/translation-hub/enqueue.js +529 -0
- package/dist/endpoints/translation-hub/failures.d.ts +12 -0
- package/dist/endpoints/translation-hub/failures.js +67 -0
- package/dist/endpoints/translation-hub/force-reset.d.ts +20 -0
- package/dist/endpoints/translation-hub/force-reset.js +144 -0
- package/dist/endpoints/translation-hub/index.d.ts +21 -0
- package/dist/endpoints/translation-hub/index.js +20 -0
- package/dist/endpoints/translation-hub/list.d.ts +40 -0
- package/dist/endpoints/translation-hub/list.js +182 -0
- package/dist/endpoints/translation-hub/preflight.d.ts +19 -0
- package/dist/endpoints/translation-hub/preflight.js +141 -0
- package/dist/endpoints/translation-hub/retry-failed.d.ts +38 -0
- package/dist/endpoints/translation-hub/retry-failed.js +235 -0
- package/dist/endpoints/translation-hub/revert.d.ts +88 -0
- package/dist/endpoints/translation-hub/revert.js +405 -0
- package/dist/endpoints/translation-hub/status.d.ts +45 -0
- package/dist/endpoints/translation-hub/status.js +391 -0
- package/dist/endpoints/translation-hub/usage-summary.d.ts +114 -0
- package/dist/endpoints/translation-hub/usage-summary.js +481 -0
- package/dist/exports/client.d.ts +6 -0
- package/dist/exports/client.js +6 -0
- package/dist/exports/components.d.ts +6 -0
- package/dist/exports/components.js +5 -0
- package/dist/exports/index.d.ts +8 -0
- package/dist/exports/index.js +7 -0
- package/dist/exports/providers.d.ts +9 -0
- package/dist/exports/providers.js +5 -0
- package/dist/exports/views-client.d.ts +23 -0
- package/dist/exports/views-client.js +22 -0
- package/dist/exports/views.d.ts +30 -0
- package/dist/exports/views.js +29 -0
- package/dist/hooks/after-change-global.d.ts +4 -0
- package/dist/hooks/after-change-global.js +109 -0
- package/dist/hooks/after-change.d.ts +16 -0
- package/dist/hooks/after-change.js +205 -0
- package/dist/hooks/after-delete.d.ts +30 -0
- package/dist/hooks/after-delete.js +95 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jobs-collection.d.ts +17 -0
- package/dist/jobs-collection.js +139 -0
- package/dist/lexical/classifier.d.ts +3 -0
- package/dist/lexical/classifier.js +108 -0
- package/dist/lexical/deserializer.d.ts +4 -0
- package/dist/lexical/deserializer.js +263 -0
- package/dist/lexical/placeholder-integrity.d.ts +6 -0
- package/dist/lexical/placeholder-integrity.js +21 -0
- package/dist/lexical/placeholders.d.ts +21 -0
- package/dist/lexical/placeholders.js +117 -0
- package/dist/lexical/serializer.d.ts +21 -0
- package/dist/lexical/serializer.js +233 -0
- package/dist/lexical/types.d.ts +32 -0
- package/dist/lexical/types.js +1 -0
- package/dist/lib/auth-diagnostics.d.ts +14 -0
- package/dist/lib/auth-diagnostics.js +19 -0
- package/dist/lib/batch-counts.d.ts +58 -0
- package/dist/lib/batch-counts.js +105 -0
- package/dist/lib/bulk-translate-migrations.d.ts +92 -0
- package/dist/lib/bulk-translate-migrations.js +153 -0
- package/dist/lib/coalescing-queue.d.ts +38 -0
- package/dist/lib/coalescing-queue.js +69 -0
- package/dist/lib/content-extractor.d.ts +16 -0
- package/dist/lib/content-extractor.js +410 -0
- package/dist/lib/content-hash.d.ts +1 -0
- package/dist/lib/content-hash.js +19 -0
- package/dist/lib/content-patcher.d.ts +15 -0
- package/dist/lib/content-patcher.js +293 -0
- package/dist/lib/cost-guards.d.ts +2 -0
- package/dist/lib/cost-guards.js +18 -0
- package/dist/lib/daily-spend-cap.d.ts +58 -0
- package/dist/lib/daily-spend-cap.js +233 -0
- package/dist/lib/effective-locales.d.ts +181 -0
- package/dist/lib/effective-locales.js +302 -0
- package/dist/lib/error-messages.d.ts +245 -0
- package/dist/lib/error-messages.js +626 -0
- package/dist/lib/events.d.ts +39 -0
- package/dist/lib/events.js +146 -0
- package/dist/lib/exclude-fields.d.ts +3 -0
- package/dist/lib/exclude-fields.js +64 -0
- package/dist/lib/field-breadcrumb.d.ts +31 -0
- package/dist/lib/field-breadcrumb.js +227 -0
- package/dist/lib/field-diff.d.ts +1 -0
- package/dist/lib/field-diff.js +25 -0
- package/dist/lib/field-empty.d.ts +2 -0
- package/dist/lib/field-empty.js +68 -0
- package/dist/lib/field-resolver.d.ts +3 -0
- package/dist/lib/field-resolver.js +164 -0
- package/dist/lib/group-soft-skips.d.ts +39 -0
- package/dist/lib/group-soft-skips.js +45 -0
- package/dist/lib/locale-merge.d.ts +44 -0
- package/dist/lib/locale-merge.js +357 -0
- package/dist/lib/locale-row-check.d.ts +30 -0
- package/dist/lib/locale-row-check.js +64 -0
- package/dist/lib/logger.d.ts +74 -0
- package/dist/lib/logger.js +97 -0
- package/dist/lib/manual-edit-guard.d.ts +128 -0
- package/dist/lib/manual-edit-guard.js +393 -0
- package/dist/lib/output-validation.d.ts +48 -0
- package/dist/lib/output-validation.js +148 -0
- package/dist/lib/payload-read.d.ts +16 -0
- package/dist/lib/payload-read.js +51 -0
- package/dist/lib/per-doc-claim.d.ts +90 -0
- package/dist/lib/per-doc-claim.js +140 -0
- package/dist/lib/per-doc-lock.d.ts +94 -0
- package/dist/lib/per-doc-lock.js +119 -0
- package/dist/lib/persist-usage.d.ts +91 -0
- package/dist/lib/persist-usage.js +116 -0
- package/dist/lib/progress-store.d.ts +103 -0
- package/dist/lib/progress-store.js +314 -0
- package/dist/lib/rate-limiter.d.ts +3 -0
- package/dist/lib/rate-limiter.js +53 -0
- package/dist/lib/snapshot-select.d.ts +43 -0
- package/dist/lib/snapshot-select.js +108 -0
- package/dist/lib/translate-prompt.d.ts +31 -0
- package/dist/lib/translate-prompt.js +66 -0
- package/dist/lib/translation-token-bucket.d.ts +57 -0
- package/dist/lib/translation-token-bucket.js +365 -0
- package/dist/lib/truncate-source-value.d.ts +1 -0
- package/dist/lib/truncate-source-value.js +27 -0
- package/dist/manual-edit-collection.d.ts +22 -0
- package/dist/manual-edit-collection.js +124 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +934 -0
- package/dist/providers/ai-sdk-adapter.d.ts +35 -0
- package/dist/providers/ai-sdk-adapter.js +100 -0
- package/dist/providers/anthropic.d.ts +31 -0
- package/dist/providers/anthropic.js +66 -0
- package/dist/providers/custom.d.ts +36 -0
- package/dist/providers/custom.js +24 -0
- package/dist/providers/gemini.d.ts +20 -0
- package/dist/providers/gemini.js +48 -0
- package/dist/providers/mock.d.ts +2 -0
- package/dist/providers/mock.js +29 -0
- package/dist/providers/openai.d.ts +28 -0
- package/dist/providers/openai.js +69 -0
- package/dist/settings-global.d.ts +74 -0
- package/dist/settings-global.js +216 -0
- package/dist/tasks/bulk-translate-coordinator.d.ts +115 -0
- package/dist/tasks/bulk-translate-coordinator.js +708 -0
- package/dist/tasks/bulk-translate-doc-task.d.ts +142 -0
- package/dist/tasks/bulk-translate-doc-task.js +1000 -0
- package/dist/tasks/bulk-translate-janitor.d.ts +87 -0
- package/dist/tasks/bulk-translate-janitor.js +311 -0
- package/dist/tasks/translate-job-task.d.ts +51 -0
- package/dist/tasks/translate-job-task.js +154 -0
- package/dist/translate.d.ts +113 -0
- package/dist/translate.js +911 -0
- package/dist/translation-daily-spend-collection.d.ts +24 -0
- package/dist/translation-daily-spend-collection.js +133 -0
- package/dist/translation-rate-limits-collection.d.ts +30 -0
- package/dist/translation-rate-limits-collection.js +144 -0
- package/dist/types.d.ts +672 -0
- package/dist/types.js +1 -0
- package/dist/usage-collection.d.ts +14 -0
- package/dist/usage-collection.js +377 -0
- package/dist/views/BulkRunsHub/BatchRow.d.ts +32 -0
- package/dist/views/BulkRunsHub/BatchRow.js +1222 -0
- package/dist/views/BulkRunsHub/BucketRow.d.ts +62 -0
- package/dist/views/BulkRunsHub/BucketRow.js +982 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.d.ts +18 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.js +331 -0
- package/dist/views/BulkRunsHub/EmptyState.d.ts +6 -0
- package/dist/views/BulkRunsHub/EmptyState.js +64 -0
- package/dist/views/BulkRunsHub/FilterBar.d.ts +16 -0
- package/dist/views/BulkRunsHub/FilterBar.js +284 -0
- package/dist/views/BulkRunsHub/InFlightBanner.d.ts +14 -0
- package/dist/views/BulkRunsHub/InFlightBanner.js +59 -0
- package/dist/views/BulkRunsHub/StatusBadge.d.ts +64 -0
- package/dist/views/BulkRunsHub/StatusBadge.js +248 -0
- package/dist/views/BulkRunsHub/SummaryStrip.d.ts +22 -0
- package/dist/views/BulkRunsHub/SummaryStrip.js +249 -0
- package/dist/views/BulkRunsHub/bucket-grouping.d.ts +200 -0
- package/dist/views/BulkRunsHub/bucket-grouping.js +344 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.d.ts +9 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.js +36 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.d.ts +5 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.js +45 -0
- package/dist/views/BulkRunsHub/index.d.ts +17 -0
- package/dist/views/BulkRunsHub/index.js +80 -0
- package/dist/views/BulkRunsHub/urlFilters.d.ts +14 -0
- package/dist/views/BulkRunsHub/urlFilters.js +50 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.d.ts +26 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.js +204 -0
- package/dist/views/BulkRunsHub/useUrlFilters.d.ts +10 -0
- package/dist/views/BulkRunsHub/useUrlFilters.js +88 -0
- package/dist/views/TranslationHub/ActiveJobs.d.ts +6 -0
- package/dist/views/TranslationHub/ActiveJobs.js +320 -0
- package/dist/views/TranslationHub/AdvancedPanel.d.ts +17 -0
- package/dist/views/TranslationHub/AdvancedPanel.js +996 -0
- package/dist/views/TranslationHub/AlertBanner.d.ts +6 -0
- package/dist/views/TranslationHub/AlertBanner.js +568 -0
- package/dist/views/TranslationHub/AuditPanel.d.ts +6 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.d.ts +44 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.js +71 -0
- package/dist/views/TranslationHub/AuditPanel.js +1367 -0
- package/dist/views/TranslationHub/BulkTranslate.types.d.ts +242 -0
- package/dist/views/TranslationHub/BulkTranslate.types.js +36 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.d.ts +19 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.js +332 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.d.ts +28 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.js +305 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.d.ts +3 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.js +42 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.d.ts +26 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.js +95 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.d.ts +22 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.js +879 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.d.ts +29 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.js +445 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.d.ts +66 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.js +161 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.d.ts +33 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.js +290 -0
- package/dist/views/TranslationHub/Hub.client.d.ts +74 -0
- package/dist/views/TranslationHub/Hub.client.js +357 -0
- package/dist/views/TranslationHub/ModelCombobox.d.ts +14 -0
- package/dist/views/TranslationHub/ModelCombobox.js +415 -0
- package/dist/views/TranslationHub/PerCollectionConfig.d.ts +10 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.d.ts +16 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.js +19 -0
- package/dist/views/TranslationHub/PerCollectionConfig.js +759 -0
- package/dist/views/TranslationHub/SettingsRail.d.ts +11 -0
- package/dist/views/TranslationHub/SettingsRail.js +382 -0
- package/dist/views/TranslationHub/StatusStrip.d.ts +6 -0
- package/dist/views/TranslationHub/StatusStrip.js +451 -0
- package/dist/views/TranslationHub/UsageTable.d.ts +6 -0
- package/dist/views/TranslationHub/UsageTable.helpers.d.ts +69 -0
- package/dist/views/TranslationHub/UsageTable.helpers.js +49 -0
- package/dist/views/TranslationHub/UsageTable.js +1240 -0
- package/dist/views/TranslationHub/alertGrouping.d.ts +70 -0
- package/dist/views/TranslationHub/alertGrouping.js +99 -0
- package/dist/views/TranslationHub/index.d.ts +20 -0
- package/dist/views/TranslationHub/index.js +109 -0
- package/dist/views/TranslationHub/tabNavigation.d.ts +53 -0
- package/dist/views/TranslationHub/tabNavigation.js +74 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.d.ts +33 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.js +124 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.d.ts +49 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.js +251 -0
- package/dist/views/TranslationHub/useFocusTrap.d.ts +6 -0
- package/dist/views/TranslationHub/useFocusTrap.js +81 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.d.ts +77 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.js +267 -0
- package/dist/views/shared/EditorError.d.ts +97 -0
- package/dist/views/shared/EditorError.js +205 -0
- package/dist/views/shared/ModelCell.d.ts +18 -0
- package/dist/views/shared/ModelCell.js +31 -0
- package/dist/views/shared/docHref.d.ts +16 -0
- package/dist/views/shared/docHref.js +26 -0
- package/dist/views/shared/fetch-error-body.d.ts +25 -0
- package/dist/views/shared/fetch-error-body.js +42 -0
- package/dist/views/shared/filterPillStyle.d.ts +35 -0
- package/dist/views/shared/filterPillStyle.js +40 -0
- package/dist/views/shared/format.d.ts +75 -0
- package/dist/views/shared/format.js +131 -0
- package/package.json +141 -0
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { categorizeFailure } from '../../lib/error-messages.js';
|
|
5
|
+
import { docHref, globalHref } from '../shared/docHref.js';
|
|
6
|
+
import { EditorError } from '../shared/EditorError.js';
|
|
7
|
+
import { formatCost, formatDuration } from '../shared/format.js';
|
|
8
|
+
import { fmtBucketAiActive, fmtBucketQueued as fmtBucketQueuedHelper, getBucketCountForFilter } from './bucket-grouping.js';
|
|
9
|
+
import { summarizeBucketFailureTypes } from './bucketFailureSummary.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Timestamp formatting — module-scoped helper. Duration and cost are
|
|
12
|
+
// rendered via the shared `formatDuration` / `formatCost` helpers in
|
|
13
|
+
// `views/shared/format.ts` (NEW-17 / NEW-18 / NEW-10).
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// ROUND5-2 (v1.2.6): predicate extracted from ROUND3-7's row-summary
|
|
16
|
+
// fix. The deep `LocaleDetailRow` panel was building an `Open →` link
|
|
17
|
+
// from `documentId` without re-running ROUND3-7's guard, so cancel-
|
|
18
|
+
// mid-flight units (which can land with `documentId === null` or
|
|
19
|
+
// the literal string `'null'`) produced `/admin/collections/posts/null`
|
|
20
|
+
// links — the same broken UX ROUND3-7 was meant to prevent, just
|
|
21
|
+
// behind one more click. Single source of truth so any new render
|
|
22
|
+
// site can re-use it.
|
|
23
|
+
function isOpenableDocId(id) {
|
|
24
|
+
return id != null && id !== 'null' && id !== '';
|
|
25
|
+
}
|
|
26
|
+
function fmtTimestamp(iso) {
|
|
27
|
+
if (!iso) return '—';
|
|
28
|
+
try {
|
|
29
|
+
return new Date(iso).toLocaleString(undefined, {
|
|
30
|
+
month: 'short',
|
|
31
|
+
day: 'numeric',
|
|
32
|
+
hour: '2-digit',
|
|
33
|
+
minute: '2-digit',
|
|
34
|
+
second: '2-digit'
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
return '—';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Tree-grouped DrillDown row component. One BucketRow per collection /
|
|
42
|
+
* global. Default expansion state controlled by parent (failed buckets
|
|
43
|
+
* open, all-success collapsed).
|
|
44
|
+
*
|
|
45
|
+
* Per UX design: bucket header shows succeeded + failed counts, top-2
|
|
46
|
+
* failure codes (when there are failures), and a "Retry N failed" CTA
|
|
47
|
+
* that maps to the existing retry-failed endpoint with a `collection`
|
|
48
|
+
* filter. NO per-bucket cost/duration (cost belongs in Audit & Cost tab;
|
|
49
|
+
* duration is meaningful at the batch level only).
|
|
50
|
+
*
|
|
51
|
+
* Per UI design: elevation-50 tint on the bucket header, red as the only
|
|
52
|
+
* saturated color on screen (success pills muted by default, failure
|
|
53
|
+
* pills saturated), 3px error-500 left border on failed doc rows.
|
|
54
|
+
*/ const BUCKET_HEADER_STYLE = {
|
|
55
|
+
background: 'var(--theme-elevation-50, #f1f5f9)',
|
|
56
|
+
borderBottom: '1px solid var(--theme-elevation-100, #e5e7eb)',
|
|
57
|
+
padding: '0.625rem 1rem',
|
|
58
|
+
fontSize: '0.8125rem',
|
|
59
|
+
fontWeight: 600,
|
|
60
|
+
color: 'var(--theme-elevation-1000)',
|
|
61
|
+
cursor: 'pointer',
|
|
62
|
+
display: 'flex',
|
|
63
|
+
alignItems: 'center',
|
|
64
|
+
gap: '0.5rem',
|
|
65
|
+
userSelect: 'none'
|
|
66
|
+
};
|
|
67
|
+
const CHEVRON_STYLE = {
|
|
68
|
+
color: 'var(--theme-elevation-500)',
|
|
69
|
+
fontSize: '0.75rem',
|
|
70
|
+
width: '0.75rem',
|
|
71
|
+
display: 'inline-block',
|
|
72
|
+
transition: 'transform 120ms ease'
|
|
73
|
+
};
|
|
74
|
+
const PILL_STYLE_BASE = {
|
|
75
|
+
fontSize: '0.6875rem',
|
|
76
|
+
fontWeight: 500,
|
|
77
|
+
display: 'inline-flex',
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
gap: '0.25rem'
|
|
80
|
+
};
|
|
81
|
+
const RETRY_BUTTON_STYLE = {
|
|
82
|
+
border: '1px solid var(--theme-error-500, #b91c1c)',
|
|
83
|
+
color: 'var(--theme-error-500, #b91c1c)',
|
|
84
|
+
background: 'transparent',
|
|
85
|
+
borderRadius: '4px',
|
|
86
|
+
padding: '0.1875rem 0.625rem',
|
|
87
|
+
fontSize: '0.6875rem',
|
|
88
|
+
fontWeight: 500,
|
|
89
|
+
cursor: 'pointer',
|
|
90
|
+
marginLeft: '1rem'
|
|
91
|
+
};
|
|
92
|
+
const GLOBAL_BADGE_STYLE = {
|
|
93
|
+
fontSize: '0.6875rem',
|
|
94
|
+
color: 'var(--theme-elevation-700)',
|
|
95
|
+
border: '1px solid var(--theme-elevation-200)',
|
|
96
|
+
borderRadius: '3px',
|
|
97
|
+
padding: '0 0.3rem',
|
|
98
|
+
marginLeft: '0.375rem',
|
|
99
|
+
fontWeight: 400
|
|
100
|
+
};
|
|
101
|
+
export const BucketRow = ({ bucket, basePath, batchId: _batchId, batchStatus, activeStatusFilter, initialExpanded, isRetrying, onRetryBucket, onExpandedChange, retryingDocKey, onRetryDoc, onLoadBucketDocs, hasMoreInBucket, isLoadingMoreInBucket })=>{
|
|
102
|
+
const [expanded, setExpanded] = useState(initialExpanded);
|
|
103
|
+
const [loading, setLoading] = useState(false);
|
|
104
|
+
const [loadError, setLoadError] = useState(null);
|
|
105
|
+
const hasFailures = bucket.failedCount > 0;
|
|
106
|
+
const hasInflight = bucket.pendingCount + bucket.runningCount > 0;
|
|
107
|
+
// v1.2.7: filter-aware "how many units match the active filter live
|
|
108
|
+
// in this bucket". Unit-based to match the historical bucket-header
|
|
109
|
+
// semantics ("8 succeeded" = 8 units, not 4 docs × 2 locales).
|
|
110
|
+
const matchingUnitCount = getBucketCountForFilter(bucket, activeStatusFilter);
|
|
111
|
+
// Filter-aware DOC count — independent from unit count above.
|
|
112
|
+
// Drives the auto-load gate + the "render empty bucket" path:
|
|
113
|
+
// we need at least one matching DOC for there to be any rows
|
|
114
|
+
// to fetch / render. A bucket with 4 succeeded units but only 4
|
|
115
|
+
// failed locales spread across the same 4 docs renders 4 rows,
|
|
116
|
+
// not 8.
|
|
117
|
+
const matchingDocCount = activeStatusFilter === 'all' ? bucket.totalDocs : bucket.docCountsByStatus[activeStatusFilter] ?? 0;
|
|
118
|
+
// The needs-load gate compares "have we loaded any rows in this
|
|
119
|
+
// bucket?" to "are there any docs in this bucket at all under the
|
|
120
|
+
// active filter?". When the filter excludes this bucket entirely
|
|
121
|
+
// (matchingDocCount === 0), we never auto-load — the parent filters
|
|
122
|
+
// the bucket out via `shouldBucketBeVisibleUnderFilter`, but the
|
|
123
|
+
// bucket can still flash through this code path during the filter
|
|
124
|
+
// transition tick.
|
|
125
|
+
const needsLoad = bucket.loadedDocs === 0 && matchingDocCount > 0;
|
|
126
|
+
// v1.2.7: filter the displayed docs by the active filter so a doc
|
|
127
|
+
// that only contains succeeded locales doesn't render under
|
|
128
|
+
// `Failed`. Buckets with mixed-status docs (one failed + one
|
|
129
|
+
// succeeded) still show the doc under both filters — that matches
|
|
130
|
+
// the bucket-count semantics (the doc lights up in both buckets).
|
|
131
|
+
const visibleDocs = [
|
|
132
|
+
...bucket.docs.values()
|
|
133
|
+
].filter((doc)=>{
|
|
134
|
+
if (activeStatusFilter === 'all') return true;
|
|
135
|
+
const target = activeStatusFilter === 'completed' ? 'success' : activeStatusFilter;
|
|
136
|
+
return doc.jobs.some((j)=>j.status === target);
|
|
137
|
+
});
|
|
138
|
+
// Notify parent on expand/collapse so it can track which buckets are
|
|
139
|
+
// open across filter changes. Idempotent on remount via the initial
|
|
140
|
+
// call below.
|
|
141
|
+
useEffect(()=>{
|
|
142
|
+
onExpandedChange?.(bucket.collection, expanded);
|
|
143
|
+
}, [
|
|
144
|
+
expanded,
|
|
145
|
+
bucket.collection,
|
|
146
|
+
onExpandedChange
|
|
147
|
+
]);
|
|
148
|
+
// Auto-load this bucket's docs the first time it's expanded if the
|
|
149
|
+
// initial paginated batch page didn't include any units from this
|
|
150
|
+
// collection (the common case for non-`posts` buckets — pagination
|
|
151
|
+
// starts at the front of the unit list which is usually one
|
|
152
|
+
// collection's slice).
|
|
153
|
+
useEffect(()=>{
|
|
154
|
+
if (!expanded || !needsLoad || loading) return;
|
|
155
|
+
let cancelled = false;
|
|
156
|
+
void (async ()=>{
|
|
157
|
+
setLoading(true);
|
|
158
|
+
setLoadError(null);
|
|
159
|
+
try {
|
|
160
|
+
await onLoadBucketDocs(bucket.collection);
|
|
161
|
+
} catch (e) {
|
|
162
|
+
if (!cancelled) {
|
|
163
|
+
setLoadError(e instanceof Error ? e.message : String(e));
|
|
164
|
+
}
|
|
165
|
+
} finally{
|
|
166
|
+
if (!cancelled) setLoading(false);
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
return ()=>{
|
|
170
|
+
cancelled = true;
|
|
171
|
+
};
|
|
172
|
+
// We deliberately depend only on (expanded, needsLoad, bucket.collection).
|
|
173
|
+
// `loading` is the in-flight guard handled inside the effect.
|
|
174
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
175
|
+
}, [
|
|
176
|
+
expanded,
|
|
177
|
+
needsLoad,
|
|
178
|
+
bucket.collection
|
|
179
|
+
]);
|
|
180
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
181
|
+
children: [
|
|
182
|
+
/*#__PURE__*/ _jsx("tr", {
|
|
183
|
+
children: /*#__PURE__*/ _jsxs("td", {
|
|
184
|
+
colSpan: 7,
|
|
185
|
+
style: {
|
|
186
|
+
padding: 0
|
|
187
|
+
},
|
|
188
|
+
children: [
|
|
189
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
190
|
+
"aria-expanded": expanded,
|
|
191
|
+
onClick: ()=>setExpanded((v)=>!v),
|
|
192
|
+
onKeyDown: (e)=>{
|
|
193
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
194
|
+
e.preventDefault();
|
|
195
|
+
setExpanded((v)=>!v);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
role: "button",
|
|
199
|
+
style: BUCKET_HEADER_STYLE,
|
|
200
|
+
tabIndex: 0,
|
|
201
|
+
children: [
|
|
202
|
+
/*#__PURE__*/ _jsx("span", {
|
|
203
|
+
style: {
|
|
204
|
+
...CHEVRON_STYLE,
|
|
205
|
+
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)'
|
|
206
|
+
},
|
|
207
|
+
children: "▶"
|
|
208
|
+
}),
|
|
209
|
+
/*#__PURE__*/ _jsx("span", {
|
|
210
|
+
style: {
|
|
211
|
+
fontWeight: 600
|
|
212
|
+
},
|
|
213
|
+
children: bucket.collection
|
|
214
|
+
}),
|
|
215
|
+
bucket.isGlobal && /*#__PURE__*/ _jsx("span", {
|
|
216
|
+
style: GLOBAL_BADGE_STYLE,
|
|
217
|
+
children: "global"
|
|
218
|
+
}),
|
|
219
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
220
|
+
style: {
|
|
221
|
+
marginLeft: 'auto',
|
|
222
|
+
display: 'flex',
|
|
223
|
+
alignItems: 'center',
|
|
224
|
+
gap: '0.75rem'
|
|
225
|
+
},
|
|
226
|
+
children: [
|
|
227
|
+
activeStatusFilter !== 'all' ? /*#__PURE__*/ _jsxs("span", {
|
|
228
|
+
style: {
|
|
229
|
+
...PILL_STYLE_BASE,
|
|
230
|
+
color: activeStatusFilter === 'failed' ? 'var(--theme-error-500, #b91c1c)' : activeStatusFilter === 'running' || activeStatusFilter === 'pending' ? 'var(--theme-warning-500, #d97706)' : activeStatusFilter === 'completed' ? 'var(--theme-success-500, #16a34a)' : 'var(--theme-elevation-600)'
|
|
231
|
+
},
|
|
232
|
+
title: `Units in this ${bucket.isGlobal ? 'global' : 'collection'} with status ${activeStatusFilter === 'completed' ? 'succeeded' : activeStatusFilter}.`,
|
|
233
|
+
children: [
|
|
234
|
+
/*#__PURE__*/ _jsx("span", {
|
|
235
|
+
style: {
|
|
236
|
+
fontSize: '0.5rem'
|
|
237
|
+
},
|
|
238
|
+
children: "●"
|
|
239
|
+
}),
|
|
240
|
+
matchingUnitCount,
|
|
241
|
+
' ',
|
|
242
|
+
activeStatusFilter === 'completed' ? 'succeeded' : activeStatusFilter
|
|
243
|
+
]
|
|
244
|
+
}) : /*#__PURE__*/ _jsxs(_Fragment, {
|
|
245
|
+
children: [
|
|
246
|
+
bucket.succeededCount > 0 && /*#__PURE__*/ _jsxs("span", {
|
|
247
|
+
style: {
|
|
248
|
+
...PILL_STYLE_BASE,
|
|
249
|
+
color: hasFailures ? 'var(--theme-elevation-600)' // muted when failure pill is also on screen
|
|
250
|
+
: 'var(--theme-success-500, #16a34a)'
|
|
251
|
+
},
|
|
252
|
+
children: [
|
|
253
|
+
/*#__PURE__*/ _jsx("span", {
|
|
254
|
+
style: {
|
|
255
|
+
fontSize: '0.5rem'
|
|
256
|
+
},
|
|
257
|
+
children: "●"
|
|
258
|
+
}),
|
|
259
|
+
bucket.succeededCount,
|
|
260
|
+
" succeeded"
|
|
261
|
+
]
|
|
262
|
+
}),
|
|
263
|
+
bucket.failedCount > 0 && /*#__PURE__*/ _jsxs("span", {
|
|
264
|
+
style: {
|
|
265
|
+
...PILL_STYLE_BASE,
|
|
266
|
+
color: 'var(--theme-error-500, #b91c1c)'
|
|
267
|
+
},
|
|
268
|
+
children: [
|
|
269
|
+
/*#__PURE__*/ _jsx("span", {
|
|
270
|
+
style: {
|
|
271
|
+
fontSize: '0.5rem'
|
|
272
|
+
},
|
|
273
|
+
children: "●"
|
|
274
|
+
}),
|
|
275
|
+
bucket.failedCount,
|
|
276
|
+
" failed"
|
|
277
|
+
]
|
|
278
|
+
}),
|
|
279
|
+
hasInflight && /*#__PURE__*/ _jsxs("span", {
|
|
280
|
+
style: {
|
|
281
|
+
...PILL_STYLE_BASE,
|
|
282
|
+
color: 'var(--theme-warning-500, #d97706)'
|
|
283
|
+
},
|
|
284
|
+
children: [
|
|
285
|
+
/*#__PURE__*/ _jsx("span", {
|
|
286
|
+
style: {
|
|
287
|
+
fontSize: '0.5rem'
|
|
288
|
+
},
|
|
289
|
+
children: "●"
|
|
290
|
+
}),
|
|
291
|
+
bucket.pendingCount + bucket.runningCount,
|
|
292
|
+
" in flight"
|
|
293
|
+
]
|
|
294
|
+
}),
|
|
295
|
+
bucket.skippedCount > 0 && /*#__PURE__*/ _jsxs("span", {
|
|
296
|
+
style: {
|
|
297
|
+
...PILL_STYLE_BASE,
|
|
298
|
+
color: 'var(--theme-elevation-600)'
|
|
299
|
+
},
|
|
300
|
+
children: [
|
|
301
|
+
bucket.skippedCount,
|
|
302
|
+
" skipped"
|
|
303
|
+
]
|
|
304
|
+
})
|
|
305
|
+
]
|
|
306
|
+
}),
|
|
307
|
+
bucket.collectionSpanMs !== null && /*#__PURE__*/ _jsxs("span", {
|
|
308
|
+
style: {
|
|
309
|
+
...PILL_STYLE_BASE,
|
|
310
|
+
color: 'var(--theme-elevation-600)',
|
|
311
|
+
fontFamily: 'monospace'
|
|
312
|
+
},
|
|
313
|
+
title: bucket.aiActiveMs !== null || bucket.queueWaitMs !== null ? `Collection span — wall-clock from this bucket's first unit start to last completion.\nAI active: ${fmtBucketAiActive(bucket)} · Queued: ${fmtBucketQueuedHelper(bucket)}\n"Queued" is the gap between worker pickup and AI execution (rate-limit / throttle wait). It's a system-level wait, not per-bucket work — buckets processed later in the same batch inherit the queue depth from earlier buckets.` : 'Wall-clock from first unit start to last completion within this collection.',
|
|
314
|
+
children: [
|
|
315
|
+
formatDuration(bucket.collectionSpanMs),
|
|
316
|
+
(bucket.aiActiveMs !== null || bucket.queueWaitMs !== null) && /*#__PURE__*/ _jsxs("span", {
|
|
317
|
+
style: {
|
|
318
|
+
marginLeft: '0.35rem',
|
|
319
|
+
fontSize: '0.625rem',
|
|
320
|
+
color: 'var(--theme-elevation-500)'
|
|
321
|
+
},
|
|
322
|
+
children: [
|
|
323
|
+
"(AI ",
|
|
324
|
+
fmtBucketAiActive(bucket),
|
|
325
|
+
" · queued ",
|
|
326
|
+
fmtBucketQueuedHelper(bucket),
|
|
327
|
+
")"
|
|
328
|
+
]
|
|
329
|
+
})
|
|
330
|
+
]
|
|
331
|
+
}),
|
|
332
|
+
hasFailures && /*#__PURE__*/ _jsx("button", {
|
|
333
|
+
disabled: isRetrying || hasInflight,
|
|
334
|
+
onClick: (e)=>{
|
|
335
|
+
e.stopPropagation();
|
|
336
|
+
onRetryBucket(bucket.collection);
|
|
337
|
+
},
|
|
338
|
+
style: {
|
|
339
|
+
...RETRY_BUTTON_STYLE,
|
|
340
|
+
opacity: isRetrying || hasInflight ? 0.5 : 1,
|
|
341
|
+
cursor: isRetrying || hasInflight ? 'not-allowed' : 'pointer'
|
|
342
|
+
},
|
|
343
|
+
title: hasInflight ? 'Wait for in-flight units to finish before retrying.' : `Retry the ${bucket.failedCount} failed unit${bucket.failedCount === 1 ? '' : 's'} in this ${bucket.isGlobal ? 'global' : 'collection'}.`,
|
|
344
|
+
type: "button",
|
|
345
|
+
children: isRetrying ? 'Retrying…' : `Retry ${bucket.failedCount} failed`
|
|
346
|
+
})
|
|
347
|
+
]
|
|
348
|
+
})
|
|
349
|
+
]
|
|
350
|
+
}),
|
|
351
|
+
expanded && bucket.distinctFailureCodes > 1 && // v1.2.7: suppress the "N different error types" summary
|
|
352
|
+
// when the active filter is non-failure — the editor is
|
|
353
|
+
// explicitly looking at succeeded / pending / etc., so the
|
|
354
|
+
// failure breakdown is noise.
|
|
355
|
+
(activeStatusFilter === 'all' || activeStatusFilter === 'failed') && (()=>{
|
|
356
|
+
// ROUND3-8: route the bucket-summary error-types through
|
|
357
|
+
// the same `categorizeFailure` helper the per-unit
|
|
358
|
+
// drill-down uses (NEW-12 / Group H). Previously the
|
|
359
|
+
// summary keyed off the raw persisted `failureCode` and
|
|
360
|
+
// rendered "Translation system is not set up" for
|
|
361
|
+
// cancel-mid-flight units while the drill-down correctly
|
|
362
|
+
// said "Stopped mid-run". Editors who didn't drill in saw
|
|
363
|
+
// a misleading config-broken signal. Threading
|
|
364
|
+
// `batchStatus` through the categorizer collapses both
|
|
365
|
+
// surfaces onto the same story. Pure helper extracted to
|
|
366
|
+
// `bucketFailureSummary.ts` so the grouping logic is
|
|
367
|
+
// unit-tested.
|
|
368
|
+
const summaryEntries = summarizeBucketFailureTypes(bucket.topFailureCodes, batchStatus);
|
|
369
|
+
// If everything collapsed to one bucket post-categorisation
|
|
370
|
+
// the "different error types" framing no longer applies —
|
|
371
|
+
// suppress the line entirely, the unit-level drill-down is
|
|
372
|
+
// the authoritative surface.
|
|
373
|
+
if (summaryEntries.length <= 1) return null;
|
|
374
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
375
|
+
style: {
|
|
376
|
+
padding: '0.375rem 1rem 0.375rem 2.25rem',
|
|
377
|
+
fontSize: '0.75rem',
|
|
378
|
+
color: 'var(--theme-warning-500, #d97706)',
|
|
379
|
+
background: 'var(--theme-elevation-0)',
|
|
380
|
+
borderBottom: '1px solid var(--theme-elevation-100)'
|
|
381
|
+
},
|
|
382
|
+
children: [
|
|
383
|
+
summaryEntries.length,
|
|
384
|
+
" different error type",
|
|
385
|
+
summaryEntries.length === 1 ? '' : 's',
|
|
386
|
+
" in this",
|
|
387
|
+
' ',
|
|
388
|
+
bucket.isGlobal ? 'global' : 'collection',
|
|
389
|
+
" —",
|
|
390
|
+
' ',
|
|
391
|
+
summaryEntries.map(({ title, count })=>`${title} (${count})`).join(', '),
|
|
392
|
+
summaryEntries.length > 2 && '. See rows for full breakdown.'
|
|
393
|
+
]
|
|
394
|
+
});
|
|
395
|
+
})()
|
|
396
|
+
]
|
|
397
|
+
})
|
|
398
|
+
}),
|
|
399
|
+
expanded && loading && /*#__PURE__*/ _jsx("tr", {
|
|
400
|
+
children: /*#__PURE__*/ _jsxs("td", {
|
|
401
|
+
colSpan: 7,
|
|
402
|
+
style: {
|
|
403
|
+
padding: '0.5rem 1rem 0.5rem 2.25rem',
|
|
404
|
+
fontSize: '0.75rem',
|
|
405
|
+
color: 'var(--theme-elevation-600)',
|
|
406
|
+
background: 'var(--theme-elevation-0)',
|
|
407
|
+
borderBottom: '1px solid var(--theme-elevation-100)'
|
|
408
|
+
},
|
|
409
|
+
children: [
|
|
410
|
+
"Loading ",
|
|
411
|
+
bucket.totalDocs,
|
|
412
|
+
" doc",
|
|
413
|
+
bucket.totalDocs === 1 ? '' : 's',
|
|
414
|
+
" in this",
|
|
415
|
+
' ',
|
|
416
|
+
bucket.isGlobal ? 'global' : 'collection',
|
|
417
|
+
"…"
|
|
418
|
+
]
|
|
419
|
+
})
|
|
420
|
+
}),
|
|
421
|
+
expanded && !loading && loadError && /*#__PURE__*/ _jsx("tr", {
|
|
422
|
+
children: /*#__PURE__*/ _jsxs("td", {
|
|
423
|
+
colSpan: 7,
|
|
424
|
+
style: {
|
|
425
|
+
padding: '0.5rem 1rem 0.5rem 2.25rem',
|
|
426
|
+
fontSize: '0.75rem',
|
|
427
|
+
color: 'var(--theme-error-500, #b91c1c)',
|
|
428
|
+
background: 'var(--theme-elevation-0)',
|
|
429
|
+
borderBottom: '1px solid var(--theme-elevation-100)'
|
|
430
|
+
},
|
|
431
|
+
children: [
|
|
432
|
+
"Could not load the docs for this ",
|
|
433
|
+
bucket.isGlobal ? 'global' : 'collection',
|
|
434
|
+
". Refresh and try again."
|
|
435
|
+
]
|
|
436
|
+
})
|
|
437
|
+
}),
|
|
438
|
+
expanded && visibleDocs.map((doc)=>/*#__PURE__*/ _jsx(DocRow, {
|
|
439
|
+
basePath: basePath,
|
|
440
|
+
batchStatus: batchStatus,
|
|
441
|
+
doc: doc,
|
|
442
|
+
isGlobal: bucket.isGlobal,
|
|
443
|
+
isRetrying: retryingDocKey === `${doc.collection}/${doc.documentId}`,
|
|
444
|
+
onRetry: ()=>onRetryDoc(doc.collection, doc.documentId)
|
|
445
|
+
}, `${doc.collection}-${doc.documentId}`)),
|
|
446
|
+
expanded && !loading && !loadError && visibleDocs.length === 0 && matchingDocCount > 0 && /*#__PURE__*/ _jsx("tr", {
|
|
447
|
+
children: /*#__PURE__*/ _jsxs("td", {
|
|
448
|
+
colSpan: 7,
|
|
449
|
+
style: {
|
|
450
|
+
padding: '0.5rem 1rem 0.5rem 2.25rem',
|
|
451
|
+
fontSize: '0.75rem',
|
|
452
|
+
color: 'var(--theme-elevation-500)',
|
|
453
|
+
background: 'var(--theme-elevation-0)',
|
|
454
|
+
borderBottom: '1px solid var(--theme-elevation-100)'
|
|
455
|
+
},
|
|
456
|
+
children: [
|
|
457
|
+
"Loading ",
|
|
458
|
+
matchingDocCount,
|
|
459
|
+
" matching doc",
|
|
460
|
+
matchingDocCount === 1 ? '' : 's',
|
|
461
|
+
" for the active filter…"
|
|
462
|
+
]
|
|
463
|
+
})
|
|
464
|
+
}),
|
|
465
|
+
expanded && !loading && !loadError && bucket.loadedDocs > 0 && hasMoreInBucket && /*#__PURE__*/ _jsx("tr", {
|
|
466
|
+
children: /*#__PURE__*/ _jsx("td", {
|
|
467
|
+
colSpan: 7,
|
|
468
|
+
style: {
|
|
469
|
+
padding: '0.5rem 1rem 0.6rem 2.25rem',
|
|
470
|
+
background: 'var(--theme-elevation-0)',
|
|
471
|
+
borderBottom: '1px solid var(--theme-elevation-100)'
|
|
472
|
+
},
|
|
473
|
+
children: /*#__PURE__*/ _jsx("button", {
|
|
474
|
+
type: "button",
|
|
475
|
+
disabled: isLoadingMoreInBucket,
|
|
476
|
+
onClick: ()=>onLoadBucketDocs(bucket.collection),
|
|
477
|
+
style: {
|
|
478
|
+
padding: '0.3rem 0.75rem',
|
|
479
|
+
background: 'transparent',
|
|
480
|
+
border: '1px solid var(--theme-elevation-200)',
|
|
481
|
+
borderRadius: '4px',
|
|
482
|
+
color: 'var(--theme-elevation-700)',
|
|
483
|
+
fontSize: '0.75rem',
|
|
484
|
+
cursor: isLoadingMoreInBucket ? 'not-allowed' : 'pointer'
|
|
485
|
+
},
|
|
486
|
+
children: isLoadingMoreInBucket ? 'Loading…' : `Load more in ${bucket.collection} (${bucket.loadedDocs} of ${bucket.totalDocs} loaded)`
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
})
|
|
490
|
+
]
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
// ---------------------------------------------------------------------------
|
|
494
|
+
// DocRow — one row per (collection, doc) with locale chips inline
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
const LOCALE_CHIP_STYLE = {
|
|
497
|
+
display: 'inline-flex',
|
|
498
|
+
alignItems: 'center',
|
|
499
|
+
gap: '0.25rem',
|
|
500
|
+
fontSize: '0.6875rem',
|
|
501
|
+
fontWeight: 500,
|
|
502
|
+
padding: '0.125rem 0.4rem',
|
|
503
|
+
borderRadius: '3px',
|
|
504
|
+
cursor: 'default'
|
|
505
|
+
};
|
|
506
|
+
const STATUS_LABEL = {
|
|
507
|
+
success: 'Succeeded',
|
|
508
|
+
failed: 'Failed',
|
|
509
|
+
pending: 'Pending',
|
|
510
|
+
running: 'Running',
|
|
511
|
+
skipped: 'Skipped',
|
|
512
|
+
reverted: 'Reverted'
|
|
513
|
+
};
|
|
514
|
+
const STATUS_COLOR = {
|
|
515
|
+
success: 'var(--theme-success-500, #16a34a)',
|
|
516
|
+
failed: 'var(--theme-error-500, #b91c1c)',
|
|
517
|
+
pending: 'var(--theme-elevation-500)',
|
|
518
|
+
running: 'var(--theme-warning-500, #d97706)',
|
|
519
|
+
skipped: 'var(--theme-elevation-500)',
|
|
520
|
+
reverted: 'var(--theme-elevation-500)'
|
|
521
|
+
};
|
|
522
|
+
const DocRow = ({ basePath, batchStatus, doc, isGlobal, isRetrying, onRetry })=>{
|
|
523
|
+
// Auto-expand any doc that has failures so the editor sees the
|
|
524
|
+
// failure detail without clicking — the UX call from the audit:
|
|
525
|
+
// "the problem hits the editor before any click."
|
|
526
|
+
const [expanded, setExpanded] = useState(doc.failedLocaleCount > 0);
|
|
527
|
+
const hasFailures = doc.failedLocaleCount > 0;
|
|
528
|
+
return /*#__PURE__*/ _jsx(_Fragment, {
|
|
529
|
+
children: /*#__PURE__*/ _jsx("tr", {
|
|
530
|
+
children: /*#__PURE__*/ _jsxs("td", {
|
|
531
|
+
colSpan: 7,
|
|
532
|
+
style: {
|
|
533
|
+
padding: '0.4rem 1rem 0.4rem 2.25rem',
|
|
534
|
+
borderBottom: '1px solid var(--theme-elevation-100)',
|
|
535
|
+
background: hasFailures ? 'var(--theme-error-50, rgba(185,28,28,0.04))' : 'var(--theme-elevation-0)',
|
|
536
|
+
borderLeft: hasFailures ? '3px solid var(--theme-error-500, #b91c1c)' : '3px solid transparent',
|
|
537
|
+
fontSize: '0.8125rem'
|
|
538
|
+
},
|
|
539
|
+
children: [
|
|
540
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
541
|
+
"aria-expanded": expanded,
|
|
542
|
+
onClick: ()=>setExpanded((v)=>!v),
|
|
543
|
+
onKeyDown: (e)=>{
|
|
544
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
545
|
+
e.preventDefault();
|
|
546
|
+
setExpanded((v)=>!v);
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
role: "button",
|
|
550
|
+
style: {
|
|
551
|
+
display: 'flex',
|
|
552
|
+
alignItems: 'center',
|
|
553
|
+
gap: '0.75rem',
|
|
554
|
+
cursor: 'pointer',
|
|
555
|
+
userSelect: 'none'
|
|
556
|
+
},
|
|
557
|
+
tabIndex: 0,
|
|
558
|
+
children: [
|
|
559
|
+
/*#__PURE__*/ _jsx("span", {
|
|
560
|
+
style: {
|
|
561
|
+
color: 'var(--theme-elevation-500)',
|
|
562
|
+
fontSize: '0.7rem',
|
|
563
|
+
width: '0.7rem',
|
|
564
|
+
display: 'inline-block',
|
|
565
|
+
transition: 'transform 120ms ease',
|
|
566
|
+
transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)'
|
|
567
|
+
},
|
|
568
|
+
children: "▶"
|
|
569
|
+
}),
|
|
570
|
+
/*#__PURE__*/ _jsx("span", {
|
|
571
|
+
style: {
|
|
572
|
+
minWidth: '5rem',
|
|
573
|
+
color: 'var(--theme-elevation-1000)'
|
|
574
|
+
},
|
|
575
|
+
children: !isOpenableDocId(doc.documentId) ? /*#__PURE__*/ _jsx("span", {
|
|
576
|
+
style: {
|
|
577
|
+
color: 'var(--theme-elevation-500)',
|
|
578
|
+
fontStyle: 'italic',
|
|
579
|
+
fontFamily: 'inherit'
|
|
580
|
+
},
|
|
581
|
+
title: "The unit's document id wasn't resolved before the batch was cancelled — no document to open.",
|
|
582
|
+
children: "(id unresolved)"
|
|
583
|
+
}) : /*#__PURE__*/ _jsx("a", {
|
|
584
|
+
// 2026-06-05: globals route through globalHref; previous
|
|
585
|
+
// code always used docHref and built
|
|
586
|
+
// `/admin/collections/<slug>/<slug>` for globals like
|
|
587
|
+
// `translation-keys` — a 404. The deeper per-locale
|
|
588
|
+
// panel below (~line 909) already had this fix; this
|
|
589
|
+
// is the per-doc row link, same fix needed here.
|
|
590
|
+
href: (isGlobal ? globalHref(basePath, doc.collection, null) : docHref(basePath, doc.collection, doc.documentId, null)) ?? '#',
|
|
591
|
+
onClick: (e)=>e.stopPropagation(),
|
|
592
|
+
rel: "noopener noreferrer",
|
|
593
|
+
style: {
|
|
594
|
+
color: 'var(--theme-text-link, #2563eb)',
|
|
595
|
+
textDecoration: 'none',
|
|
596
|
+
fontFamily: 'monospace'
|
|
597
|
+
},
|
|
598
|
+
target: "_blank",
|
|
599
|
+
children: isGlobal ? doc.collection : `#${doc.documentId}`
|
|
600
|
+
})
|
|
601
|
+
}),
|
|
602
|
+
/*#__PURE__*/ _jsx("div", {
|
|
603
|
+
style: {
|
|
604
|
+
display: 'flex',
|
|
605
|
+
flexWrap: 'wrap',
|
|
606
|
+
gap: '0.4rem'
|
|
607
|
+
},
|
|
608
|
+
children: doc.jobs.map((job)=>/*#__PURE__*/ _jsx(LocaleChip, {
|
|
609
|
+
job: job
|
|
610
|
+
}, job.locale))
|
|
611
|
+
}),
|
|
612
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
613
|
+
style: {
|
|
614
|
+
marginLeft: 'auto',
|
|
615
|
+
display: 'flex',
|
|
616
|
+
alignItems: 'center',
|
|
617
|
+
gap: '1rem',
|
|
618
|
+
fontSize: '0.75rem',
|
|
619
|
+
color: 'var(--theme-elevation-600)',
|
|
620
|
+
fontFamily: 'monospace'
|
|
621
|
+
},
|
|
622
|
+
children: [
|
|
623
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
624
|
+
title: doc.docAiActiveMs !== null && doc.docQueueWaitMs !== null ? `Doc span — from first locale start to last completion.\nAI active: ${formatDuration(doc.docAiActiveMs)} · Queue wait: ${formatDuration(doc.docQueueWaitMs)}${doc.maxAttempts > 1 ? '\nIncludes retry attempts.' : ''}` : "Doc span — from this doc's first locale start to its last completion. Includes any throttle queue wait. Doc-level AI compute time wasn't recorded on this batch (legacy data).",
|
|
625
|
+
children: [
|
|
626
|
+
formatDuration(doc.docSpanMs),
|
|
627
|
+
doc.docAiActiveMs !== null && doc.docQueueWaitMs !== null && /*#__PURE__*/ _jsxs("span", {
|
|
628
|
+
style: {
|
|
629
|
+
marginLeft: '0.35rem',
|
|
630
|
+
fontSize: '0.625rem',
|
|
631
|
+
color: 'var(--theme-elevation-500)'
|
|
632
|
+
},
|
|
633
|
+
children: [
|
|
634
|
+
"(AI ",
|
|
635
|
+
formatDuration(doc.docAiActiveMs),
|
|
636
|
+
")"
|
|
637
|
+
]
|
|
638
|
+
}),
|
|
639
|
+
doc.maxAttempts > 1 && /*#__PURE__*/ _jsx("span", {
|
|
640
|
+
style: {
|
|
641
|
+
marginLeft: '0.35rem',
|
|
642
|
+
fontSize: '0.625rem',
|
|
643
|
+
color: 'var(--theme-warning-500, #d97706)'
|
|
644
|
+
},
|
|
645
|
+
children: "(incl. retries)"
|
|
646
|
+
})
|
|
647
|
+
]
|
|
648
|
+
}),
|
|
649
|
+
/*#__PURE__*/ _jsx("span", {
|
|
650
|
+
title: "Total cost across all locales for this doc",
|
|
651
|
+
children: formatCost(doc.totalCostUsd)
|
|
652
|
+
}),
|
|
653
|
+
doc.maxAttempts > 1 && /*#__PURE__*/ _jsxs("span", {
|
|
654
|
+
style: {
|
|
655
|
+
color: 'var(--theme-warning-500, #d97706)'
|
|
656
|
+
},
|
|
657
|
+
title: "Highest attempt count across this doc's locales",
|
|
658
|
+
children: [
|
|
659
|
+
doc.maxAttempts,
|
|
660
|
+
"× attempts"
|
|
661
|
+
]
|
|
662
|
+
}),
|
|
663
|
+
hasFailures && /*#__PURE__*/ _jsx("button", {
|
|
664
|
+
disabled: isRetrying,
|
|
665
|
+
onClick: (e)=>{
|
|
666
|
+
e.stopPropagation();
|
|
667
|
+
onRetry();
|
|
668
|
+
},
|
|
669
|
+
style: {
|
|
670
|
+
border: '1px solid var(--theme-error-500, #b91c1c)',
|
|
671
|
+
color: 'var(--theme-error-500, #b91c1c)',
|
|
672
|
+
background: 'transparent',
|
|
673
|
+
borderRadius: '4px',
|
|
674
|
+
padding: '0.1rem 0.5rem',
|
|
675
|
+
fontSize: '0.6875rem',
|
|
676
|
+
fontWeight: 500,
|
|
677
|
+
cursor: isRetrying ? 'wait' : 'pointer',
|
|
678
|
+
opacity: isRetrying ? 0.5 : 1
|
|
679
|
+
},
|
|
680
|
+
title: `Retry the ${doc.failedLocaleCount} failed locale${doc.failedLocaleCount === 1 ? '' : 's'} for this doc.`,
|
|
681
|
+
type: "button",
|
|
682
|
+
children: isRetrying ? 'Retrying…' : `Retry ${doc.failedLocaleCount}`
|
|
683
|
+
})
|
|
684
|
+
]
|
|
685
|
+
})
|
|
686
|
+
]
|
|
687
|
+
}),
|
|
688
|
+
expanded && /*#__PURE__*/ _jsx("div", {
|
|
689
|
+
style: {
|
|
690
|
+
marginTop: '0.5rem',
|
|
691
|
+
display: 'flex',
|
|
692
|
+
flexDirection: 'column',
|
|
693
|
+
gap: '0.4rem'
|
|
694
|
+
},
|
|
695
|
+
children: doc.jobs.map((job)=>/*#__PURE__*/ _jsx(LocaleDetailRow, {
|
|
696
|
+
basePath: basePath,
|
|
697
|
+
batchStatus: batchStatus,
|
|
698
|
+
collection: doc.collection,
|
|
699
|
+
documentId: doc.documentId,
|
|
700
|
+
isGlobal: isGlobal,
|
|
701
|
+
job: job
|
|
702
|
+
}, `detail-${job.locale}`))
|
|
703
|
+
})
|
|
704
|
+
]
|
|
705
|
+
})
|
|
706
|
+
})
|
|
707
|
+
});
|
|
708
|
+
};
|
|
709
|
+
const LocaleDetailRow = ({ basePath, batchStatus, collection, documentId, isGlobal, job })=>{
|
|
710
|
+
const isFailed = job.status === 'failed';
|
|
711
|
+
// NEW-12 (v1.2.6): route through the categorizer so cancel-mid-flight
|
|
712
|
+
// units stop showing "Translation system is not set up" copy when 90%
|
|
713
|
+
// of the batch succeeded. The categorizer also corrects pre-v1.2.6
|
|
714
|
+
// persisted rows where `failureCode` says `permanent.config` but the
|
|
715
|
+
// raw message clearly says "Provider returned undefined / non-finite".
|
|
716
|
+
const categorized = categorizeFailure({
|
|
717
|
+
failureCode: job.failureCode,
|
|
718
|
+
failureMessage: job.failureMessage,
|
|
719
|
+
batchStatus,
|
|
720
|
+
attemptCount: job.attempts
|
|
721
|
+
});
|
|
722
|
+
const code = categorized.editorCode;
|
|
723
|
+
// ROUND5-2 (v1.2.6): suppress the `Open →` link when the doc id
|
|
724
|
+
// wasn't resolved (cancel-mid-flight). ROUND3-7 patched the row
|
|
725
|
+
// summary; this is the deeper detail panel. Building docHref with
|
|
726
|
+
// a null id produced `/admin/collections/posts/null?locale=es`
|
|
727
|
+
// which lands the editor on Payload's "document not found" page.
|
|
728
|
+
// v1.2.7: globals are addressed by slug only (`/admin/globals/<slug>`),
|
|
729
|
+
// never by id. Pre-v1.2.7 the link always used `docHref` which built
|
|
730
|
+
// `/admin/collections/translation-keys/translation-keys?locale=es` —
|
|
731
|
+
// a 404 because translation-keys is a global. Route through the right
|
|
732
|
+
// builder per surface kind.
|
|
733
|
+
const localeHref = isOpenableDocId(documentId) ? isGlobal ? globalHref(basePath, collection, job.locale) : docHref(basePath, collection, documentId, job.locale) : null;
|
|
734
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
735
|
+
style: {
|
|
736
|
+
padding: '0.5rem 0.75rem',
|
|
737
|
+
background: 'var(--theme-elevation-50, #f8fafc)',
|
|
738
|
+
border: '1px solid var(--theme-elevation-100)',
|
|
739
|
+
borderLeft: isFailed ? '3px solid var(--theme-error-500, #b91c1c)' : `3px solid ${STATUS_COLOR[job.status] ?? 'var(--theme-elevation-200)'}`,
|
|
740
|
+
borderRadius: '4px',
|
|
741
|
+
fontSize: '0.75rem'
|
|
742
|
+
},
|
|
743
|
+
children: [
|
|
744
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
745
|
+
style: {
|
|
746
|
+
display: 'grid',
|
|
747
|
+
gridTemplateColumns: 'minmax(2.5rem, auto) minmax(5rem, auto) repeat(3, minmax(0, 1fr)) auto',
|
|
748
|
+
gap: '1rem',
|
|
749
|
+
alignItems: 'center'
|
|
750
|
+
},
|
|
751
|
+
children: [
|
|
752
|
+
/*#__PURE__*/ _jsx("span", {
|
|
753
|
+
style: {
|
|
754
|
+
fontFamily: 'monospace',
|
|
755
|
+
fontWeight: 600,
|
|
756
|
+
color: 'var(--theme-elevation-1000)'
|
|
757
|
+
},
|
|
758
|
+
children: job.locale
|
|
759
|
+
}),
|
|
760
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
761
|
+
style: {
|
|
762
|
+
color: STATUS_COLOR[job.status] ?? 'var(--theme-elevation-700)',
|
|
763
|
+
fontWeight: 500,
|
|
764
|
+
display: 'inline-flex',
|
|
765
|
+
alignItems: 'center',
|
|
766
|
+
gap: '0.3rem'
|
|
767
|
+
},
|
|
768
|
+
children: [
|
|
769
|
+
/*#__PURE__*/ _jsx("span", {
|
|
770
|
+
style: {
|
|
771
|
+
fontSize: '0.5rem'
|
|
772
|
+
},
|
|
773
|
+
children: "●"
|
|
774
|
+
}),
|
|
775
|
+
STATUS_LABEL[job.status] ?? job.status
|
|
776
|
+
]
|
|
777
|
+
}),
|
|
778
|
+
/*#__PURE__*/ _jsx(DataField, {
|
|
779
|
+
label: "AI time",
|
|
780
|
+
value: formatDuration(job.processingDurationMs),
|
|
781
|
+
hint: "Provider call latency. Excludes throttle queue wait — that lives at the collection level."
|
|
782
|
+
}),
|
|
783
|
+
/*#__PURE__*/ _jsx(DataField, {
|
|
784
|
+
label: "Cost",
|
|
785
|
+
value: formatCost(job.costUsd),
|
|
786
|
+
hint: "USD billed for this locale's translation"
|
|
787
|
+
}),
|
|
788
|
+
/*#__PURE__*/ _jsx(DataField, {
|
|
789
|
+
label: "Attempts",
|
|
790
|
+
value: String(job.attempts),
|
|
791
|
+
hint: "Worker attempts (>1 means retried). Matches the count in the persistent-failure alert.",
|
|
792
|
+
tone: job.attempts > 1 ? 'warning' : 'default'
|
|
793
|
+
}),
|
|
794
|
+
localeHref && /*#__PURE__*/ _jsx("a", {
|
|
795
|
+
href: localeHref,
|
|
796
|
+
onClick: (e)=>e.stopPropagation(),
|
|
797
|
+
rel: "noopener noreferrer",
|
|
798
|
+
style: {
|
|
799
|
+
color: 'var(--theme-text-link, #2563eb)',
|
|
800
|
+
textDecoration: 'none',
|
|
801
|
+
fontSize: '0.7rem',
|
|
802
|
+
fontWeight: 500
|
|
803
|
+
},
|
|
804
|
+
target: "_blank",
|
|
805
|
+
title: `Open this document in ${job.locale}`,
|
|
806
|
+
children: "Open →"
|
|
807
|
+
})
|
|
808
|
+
]
|
|
809
|
+
}),
|
|
810
|
+
(job.startedAt || job.completedAt) && /*#__PURE__*/ _jsxs("div", {
|
|
811
|
+
style: {
|
|
812
|
+
marginTop: '0.35rem',
|
|
813
|
+
fontSize: '0.65rem',
|
|
814
|
+
color: 'var(--theme-elevation-500)',
|
|
815
|
+
fontFamily: 'monospace',
|
|
816
|
+
display: 'flex',
|
|
817
|
+
gap: '1rem'
|
|
818
|
+
},
|
|
819
|
+
children: [
|
|
820
|
+
job.startedAt && /*#__PURE__*/ _jsxs("span", {
|
|
821
|
+
children: [
|
|
822
|
+
"Started: ",
|
|
823
|
+
fmtTimestamp(job.startedAt)
|
|
824
|
+
]
|
|
825
|
+
}),
|
|
826
|
+
job.completedAt && /*#__PURE__*/ _jsxs("span", {
|
|
827
|
+
children: [
|
|
828
|
+
"Completed: ",
|
|
829
|
+
fmtTimestamp(job.completedAt)
|
|
830
|
+
]
|
|
831
|
+
})
|
|
832
|
+
]
|
|
833
|
+
}),
|
|
834
|
+
isFailed && /*#__PURE__*/ _jsx("div", {
|
|
835
|
+
style: {
|
|
836
|
+
marginTop: '0.5rem'
|
|
837
|
+
},
|
|
838
|
+
children: /*#__PURE__*/ _jsx(EditorError, {
|
|
839
|
+
code: code,
|
|
840
|
+
compact: true,
|
|
841
|
+
context: {
|
|
842
|
+
locale: job.locale
|
|
843
|
+
},
|
|
844
|
+
details: categorized.details
|
|
845
|
+
})
|
|
846
|
+
})
|
|
847
|
+
]
|
|
848
|
+
});
|
|
849
|
+
};
|
|
850
|
+
// One column of label + value in the LocaleDetailRow grid.
|
|
851
|
+
const DataField = ({ label, value, hint, tone = 'default' })=>/*#__PURE__*/ _jsxs("span", {
|
|
852
|
+
style: {
|
|
853
|
+
display: 'flex',
|
|
854
|
+
flexDirection: 'column',
|
|
855
|
+
gap: '0.1rem',
|
|
856
|
+
fontSize: '0.7rem'
|
|
857
|
+
},
|
|
858
|
+
title: hint,
|
|
859
|
+
children: [
|
|
860
|
+
/*#__PURE__*/ _jsx("span", {
|
|
861
|
+
style: {
|
|
862
|
+
color: 'var(--theme-elevation-500)',
|
|
863
|
+
textTransform: 'uppercase',
|
|
864
|
+
letterSpacing: '0.03em',
|
|
865
|
+
fontSize: '0.6rem',
|
|
866
|
+
fontWeight: 600
|
|
867
|
+
},
|
|
868
|
+
children: label
|
|
869
|
+
}),
|
|
870
|
+
/*#__PURE__*/ _jsx("span", {
|
|
871
|
+
style: {
|
|
872
|
+
color: tone === 'warning' ? 'var(--theme-warning-500, #d97706)' : 'var(--theme-elevation-900)',
|
|
873
|
+
fontFamily: 'monospace'
|
|
874
|
+
},
|
|
875
|
+
children: value
|
|
876
|
+
})
|
|
877
|
+
]
|
|
878
|
+
});
|
|
879
|
+
// ---------------------------------------------------------------------------
|
|
880
|
+
// LocaleChip — colored pill per locale, rich tooltip
|
|
881
|
+
// ---------------------------------------------------------------------------
|
|
882
|
+
const STATUS_TO_CHIP = {
|
|
883
|
+
success: {
|
|
884
|
+
background: 'transparent',
|
|
885
|
+
color: 'var(--theme-elevation-700)',
|
|
886
|
+
dot: 'var(--theme-success-500, #16a34a)',
|
|
887
|
+
glyph: '●'
|
|
888
|
+
},
|
|
889
|
+
failed: {
|
|
890
|
+
background: 'var(--theme-error-100, #fee2e2)',
|
|
891
|
+
color: 'var(--theme-error-500, #b91c1c)',
|
|
892
|
+
dot: 'var(--theme-error-500, #b91c1c)',
|
|
893
|
+
glyph: '●'
|
|
894
|
+
},
|
|
895
|
+
// v1.2.5: outlined dot + faded text so pending is unambiguously
|
|
896
|
+
// "queued, hasn't started." Pre-1.2.5 pending rendered identical
|
|
897
|
+
// to success (same transparent bg, same filled glyph, same elev-700
|
|
898
|
+
// text) — editors couldn't tell finished work from queued work.
|
|
899
|
+
pending: {
|
|
900
|
+
background: 'transparent',
|
|
901
|
+
color: 'var(--theme-elevation-500)',
|
|
902
|
+
dot: 'var(--theme-elevation-500)',
|
|
903
|
+
glyph: '○'
|
|
904
|
+
},
|
|
905
|
+
// v1.2.5: animate the dot so an editor can see "this is actively
|
|
906
|
+
// running right now" without needing the chip to update.
|
|
907
|
+
running: {
|
|
908
|
+
background: 'var(--theme-warning-100, #fef3c7)',
|
|
909
|
+
color: 'var(--theme-warning-500, #d97706)',
|
|
910
|
+
dot: 'var(--theme-warning-500, #d97706)',
|
|
911
|
+
glyph: '●',
|
|
912
|
+
animate: true
|
|
913
|
+
},
|
|
914
|
+
skipped: {
|
|
915
|
+
background: 'transparent',
|
|
916
|
+
color: 'var(--theme-elevation-600)',
|
|
917
|
+
dot: 'var(--theme-elevation-500)',
|
|
918
|
+
glyph: '●'
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
const LocaleChip = ({ job })=>{
|
|
922
|
+
const chip = STATUS_TO_CHIP[job.status] ?? STATUS_TO_CHIP.skipped;
|
|
923
|
+
// Rich tooltip — quick stats without expanding the row. Falls back to
|
|
924
|
+
// a short summary when we don't have processing/cost data on legacy
|
|
925
|
+
// rows.
|
|
926
|
+
const parts = [
|
|
927
|
+
`${job.locale} — ${STATUS_LABEL[job.status] ?? job.status}`
|
|
928
|
+
];
|
|
929
|
+
if (typeof job.processingDurationMs === 'number' && job.processingDurationMs > 0) {
|
|
930
|
+
parts.push(formatDuration(job.processingDurationMs));
|
|
931
|
+
}
|
|
932
|
+
if (typeof job.costUsd === 'number' && job.costUsd > 0) {
|
|
933
|
+
parts.push(formatCost(job.costUsd));
|
|
934
|
+
}
|
|
935
|
+
if (job.attempts > 1) {
|
|
936
|
+
parts.push(`${job.attempts} attempts`);
|
|
937
|
+
}
|
|
938
|
+
if (job.failureCode) {
|
|
939
|
+
parts.push(`code: ${job.failureCode}`);
|
|
940
|
+
}
|
|
941
|
+
return /*#__PURE__*/ _jsxs("span", {
|
|
942
|
+
style: {
|
|
943
|
+
...LOCALE_CHIP_STYLE,
|
|
944
|
+
background: chip.background,
|
|
945
|
+
color: chip.color,
|
|
946
|
+
cursor: 'default',
|
|
947
|
+
border: '1px solid transparent',
|
|
948
|
+
fontFamily: 'monospace'
|
|
949
|
+
},
|
|
950
|
+
title: parts.join(' · '),
|
|
951
|
+
children: [
|
|
952
|
+
/*#__PURE__*/ _jsx("span", {
|
|
953
|
+
style: {
|
|
954
|
+
fontSize: '0.5rem',
|
|
955
|
+
color: chip.dot,
|
|
956
|
+
...chip.animate ? {
|
|
957
|
+
animation: 'translation-hub-pulse 1.2s ease-in-out infinite'
|
|
958
|
+
} : null
|
|
959
|
+
},
|
|
960
|
+
children: chip.glyph
|
|
961
|
+
}),
|
|
962
|
+
job.locale
|
|
963
|
+
]
|
|
964
|
+
});
|
|
965
|
+
};
|
|
966
|
+
// v1.2.5: lightweight keyframe inject. Defining `@keyframes` inline
|
|
967
|
+
// via a styled wrapper is the cheapest cross-theme path — Payload's
|
|
968
|
+
// admin shell doesn't expose a global stylesheet hook for plugins.
|
|
969
|
+
// Mounted once at the bottom of LocaleChip so any consumer that
|
|
970
|
+
// renders the chip gets the animation rule installed.
|
|
971
|
+
const PULSE_KEYFRAMES = `
|
|
972
|
+
@keyframes translation-hub-pulse {
|
|
973
|
+
0%, 100% { opacity: 1; }
|
|
974
|
+
50% { opacity: 0.35; }
|
|
975
|
+
}
|
|
976
|
+
`;
|
|
977
|
+
if (typeof document !== 'undefined' && !document.getElementById('translation-hub-pulse-keyframes')) {
|
|
978
|
+
const style = document.createElement('style');
|
|
979
|
+
style.id = 'translation-hub-pulse-keyframes';
|
|
980
|
+
style.textContent = PULSE_KEYFRAMES;
|
|
981
|
+
document.head.appendChild(style);
|
|
982
|
+
}
|