@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,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group a flat list of `BatchJobSummary` units into a 2-level tree:
|
|
3
|
+
*
|
|
4
|
+
* Bucket (collection or global)
|
|
5
|
+
* └── Document
|
|
6
|
+
* └── Locales (rendered inline as chips, not as separate rows)
|
|
7
|
+
*
|
|
8
|
+
* Editor mental model is collection-shaped, not unit-shaped — direct quote:
|
|
9
|
+
* "let's say 50 docs from one collection fail but 150 are completed maybe
|
|
10
|
+
* I want to trigger retry for those 50 easily." The flat list doesn't
|
|
11
|
+
* surface that grouping; this helper does.
|
|
12
|
+
*
|
|
13
|
+
* Heuristic for distinguishing globals from collections: the worker stores
|
|
14
|
+
* `documentId === collection` for globals (the slug doubles as the id),
|
|
15
|
+
* whereas collections have a numeric / UUID document id. Confirmed against
|
|
16
|
+
* `api.ts:translateGlobal` which returns
|
|
17
|
+
* `{ documentId: options.global, collection: options.global, ... }`.
|
|
18
|
+
*/ import { formatDuration } from '../shared/format.js';
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the bucket header count for the active filter pill. Returns a
|
|
21
|
+
* UNIT-level count (the historical semantics — `succeededCount`,
|
|
22
|
+
* `failedCount` etc. on the bucket are unit-based). When the filter is
|
|
23
|
+
* `all`, returns `totalUnits`. Otherwise returns the matching scalar.
|
|
24
|
+
*
|
|
25
|
+
* Doc-level counts (`docCountsByStatus`) stay on the bucket for callers
|
|
26
|
+
* that need "how many distinct docs match this filter" — e.g. the
|
|
27
|
+
* `shouldBucketBeVisibleUnderFilter` rule below, which has to know
|
|
28
|
+
* whether any docs at all will render under the active filter.
|
|
29
|
+
*/ export function getBucketCountForFilter(bucket, filter) {
|
|
30
|
+
switch(filter){
|
|
31
|
+
case 'all':
|
|
32
|
+
return bucket.totalUnits;
|
|
33
|
+
case 'completed':
|
|
34
|
+
return bucket.succeededCount;
|
|
35
|
+
case 'failed':
|
|
36
|
+
return bucket.failedCount;
|
|
37
|
+
case 'pending':
|
|
38
|
+
return bucket.pendingCount;
|
|
39
|
+
case 'running':
|
|
40
|
+
return bucket.runningCount;
|
|
41
|
+
case 'skipped':
|
|
42
|
+
return bucket.skippedCount;
|
|
43
|
+
case 'reverted':
|
|
44
|
+
return bucket.revertedCount;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decide whether a bucket should render at all under the active filter.
|
|
49
|
+
* Hides buckets that have zero matching docs when a non-`all` filter is
|
|
50
|
+
* active — the previous behaviour rendered the bucket header with stale
|
|
51
|
+
* unfiltered counts, contradicting the global "No units match" message
|
|
52
|
+
* below. With this rule the entire bucket disappears, and the global
|
|
53
|
+
* empty state surfaces only when EVERY bucket is empty.
|
|
54
|
+
*
|
|
55
|
+
* Uses the DOC count rather than the unit count because the expanded
|
|
56
|
+
* panel renders one row per doc. A bucket with one doc whose only
|
|
57
|
+
* failed locale we'd be looking at is still worth showing — but only
|
|
58
|
+
* if at least one such doc exists.
|
|
59
|
+
*/ export function shouldBucketBeVisibleUnderFilter(bucket, filter) {
|
|
60
|
+
if (filter === 'all') return true;
|
|
61
|
+
return (bucket.docCountsByStatus[filter] ?? 0) > 0;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Group the flat `jobs` array into buckets, using backend
|
|
65
|
+
* `countsByCollection` for the authoritative per-bucket totals.
|
|
66
|
+
*
|
|
67
|
+
* Two passes:
|
|
68
|
+
* 1. Seed every bucket from `countsByCollection` (the FULL totals
|
|
69
|
+
* across the batch — not just the paginated `jobs` page). This
|
|
70
|
+
* ensures bucket headers show correct counts even when the
|
|
71
|
+
* `jobs` page only contains a subset.
|
|
72
|
+
* 2. Walk `jobs` and attach the loaded docs to their buckets.
|
|
73
|
+
* `bucket.loadedDocs < bucket.totalDocs` when the page hasn't
|
|
74
|
+
* loaded every doc in the bucket — UI surfaces this gap.
|
|
75
|
+
*
|
|
76
|
+
* Sort order:
|
|
77
|
+
* 1. Buckets with failures come first (descending failure count).
|
|
78
|
+
* 2. Then collections alphabetically.
|
|
79
|
+
* 3. Then globals alphabetically (any bucket with `isGlobal: true`).
|
|
80
|
+
*
|
|
81
|
+
* Within a bucket, docs are kept in their original insertion order from
|
|
82
|
+
* the `jobs` array — Payload's status endpoint already sorts by unit id
|
|
83
|
+
* (closely aligned with creation order), which is a reasonable default.
|
|
84
|
+
*/ export function groupJobsIntoBuckets(jobs, countsByCollection = {}) {
|
|
85
|
+
const byCollection = new Map();
|
|
86
|
+
// Pass 1: seed every bucket from the backend's authoritative totals.
|
|
87
|
+
// `countsByCollection` is the only reliable source for total docs per
|
|
88
|
+
// collection — the paginated jobs page may miss most of them.
|
|
89
|
+
for (const [collection, counts] of Object.entries(countsByCollection)){
|
|
90
|
+
// v1.2.7: `docCountsByStatus` is optional on pre-1.2.7 backends —
|
|
91
|
+
// derive a best-effort shape from the scalar unit counts so the
|
|
92
|
+
// filter-aware bucket header still renders something sensible (it
|
|
93
|
+
// becomes unit-count rather than doc-count, but never zeroes out
|
|
94
|
+
// a populated bucket).
|
|
95
|
+
const docCountsByStatus = counts.docCountsByStatus ?? {
|
|
96
|
+
all: counts.totalDocs,
|
|
97
|
+
pending: counts.pending,
|
|
98
|
+
running: counts.running,
|
|
99
|
+
completed: counts.succeeded,
|
|
100
|
+
failed: counts.failed,
|
|
101
|
+
skipped: counts.skipped,
|
|
102
|
+
reverted: counts.reverted
|
|
103
|
+
};
|
|
104
|
+
byCollection.set(collection, {
|
|
105
|
+
collection,
|
|
106
|
+
isGlobal: counts.isGlobal,
|
|
107
|
+
docs: new Map(),
|
|
108
|
+
totalUnits: counts.total,
|
|
109
|
+
succeededCount: counts.succeeded,
|
|
110
|
+
failedCount: counts.failed,
|
|
111
|
+
pendingCount: counts.pending,
|
|
112
|
+
runningCount: counts.running,
|
|
113
|
+
skippedCount: counts.skipped,
|
|
114
|
+
revertedCount: counts.reverted,
|
|
115
|
+
totalDocs: counts.totalDocs,
|
|
116
|
+
loadedDocs: 0,
|
|
117
|
+
topFailureCodes: counts.topFailureCodes,
|
|
118
|
+
distinctFailureCodes: counts.distinctFailureCodes,
|
|
119
|
+
collectionSpanMs: counts.collectionSpanMs,
|
|
120
|
+
aiActiveMs: counts.aiActiveMs,
|
|
121
|
+
queueWaitMs: counts.queueWaitMs,
|
|
122
|
+
docCountsByStatus
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// Pass 2: attach loaded docs to their buckets. Also handle the legacy
|
|
126
|
+
// path where `countsByCollection` is empty (e.g. older API response) —
|
|
127
|
+
// fall back to bucket creation from jobs as best-effort.
|
|
128
|
+
for (const job of jobs){
|
|
129
|
+
const collection = job.collection;
|
|
130
|
+
const isGlobal = job.documentId === job.collection;
|
|
131
|
+
let bucket = byCollection.get(collection);
|
|
132
|
+
if (!bucket) {
|
|
133
|
+
// Fallback: no backend counts for this collection (legacy or
|
|
134
|
+
// race). Seed from jobs only — counts will reflect loaded page.
|
|
135
|
+
bucket = {
|
|
136
|
+
collection,
|
|
137
|
+
isGlobal,
|
|
138
|
+
docs: new Map(),
|
|
139
|
+
totalUnits: 0,
|
|
140
|
+
succeededCount: 0,
|
|
141
|
+
failedCount: 0,
|
|
142
|
+
pendingCount: 0,
|
|
143
|
+
runningCount: 0,
|
|
144
|
+
skippedCount: 0,
|
|
145
|
+
revertedCount: 0,
|
|
146
|
+
totalDocs: 0,
|
|
147
|
+
loadedDocs: 0,
|
|
148
|
+
topFailureCodes: [],
|
|
149
|
+
distinctFailureCodes: 0,
|
|
150
|
+
collectionSpanMs: null,
|
|
151
|
+
aiActiveMs: null,
|
|
152
|
+
queueWaitMs: null,
|
|
153
|
+
docCountsByStatus: {
|
|
154
|
+
all: 0,
|
|
155
|
+
pending: 0,
|
|
156
|
+
running: 0,
|
|
157
|
+
completed: 0,
|
|
158
|
+
failed: 0,
|
|
159
|
+
skipped: 0,
|
|
160
|
+
reverted: 0
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
byCollection.set(collection, bucket);
|
|
164
|
+
}
|
|
165
|
+
let doc = bucket.docs.get(job.documentId);
|
|
166
|
+
if (!doc) {
|
|
167
|
+
doc = {
|
|
168
|
+
collection,
|
|
169
|
+
documentId: job.documentId,
|
|
170
|
+
jobs: [],
|
|
171
|
+
failedLocaleCount: 0,
|
|
172
|
+
succeededLocaleCount: 0,
|
|
173
|
+
docSpanMs: null,
|
|
174
|
+
docAiActiveMs: null,
|
|
175
|
+
docQueueWaitMs: null,
|
|
176
|
+
totalCostUsd: 0,
|
|
177
|
+
maxAttempts: 0
|
|
178
|
+
};
|
|
179
|
+
bucket.docs.set(job.documentId, doc);
|
|
180
|
+
bucket.loadedDocs += 1;
|
|
181
|
+
}
|
|
182
|
+
doc.jobs.push(job);
|
|
183
|
+
// Per-locale aggregates rolled up to per-doc.
|
|
184
|
+
if (job.status === 'success') doc.succeededLocaleCount += 1;
|
|
185
|
+
else if (job.status === 'failed') doc.failedLocaleCount += 1;
|
|
186
|
+
if (typeof job.processingDurationMs === 'number' && job.processingDurationMs > 0) {
|
|
187
|
+
doc.docAiActiveMs = (doc.docAiActiveMs ?? 0) + job.processingDurationMs;
|
|
188
|
+
}
|
|
189
|
+
if (typeof job.costUsd === 'number') {
|
|
190
|
+
doc.totalCostUsd += job.costUsd;
|
|
191
|
+
}
|
|
192
|
+
if (job.attempts > doc.maxAttempts) doc.maxAttempts = job.attempts;
|
|
193
|
+
// When countsByCollection wasn't provided, also bump the bucket
|
|
194
|
+
// counts from jobs (legacy fallback).
|
|
195
|
+
const hasBackendCounts = bucket.totalUnits > 0;
|
|
196
|
+
if (!hasBackendCounts) {
|
|
197
|
+
bucket.totalUnits += 1;
|
|
198
|
+
if (job.status === 'success') bucket.succeededCount += 1;
|
|
199
|
+
else if (job.status === 'failed') bucket.failedCount += 1;
|
|
200
|
+
else if (job.status === 'pending') bucket.pendingCount += 1;
|
|
201
|
+
else if (job.status === 'running') bucket.runningCount += 1;
|
|
202
|
+
else if (job.status === 'skipped') bucket.skippedCount += 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// v1.2.7: legacy-mode docCountsByStatus rebuild — when the backend
|
|
206
|
+
// didn't provide `docCountsByStatus` we ALSO compute it from the
|
|
207
|
+
// loaded jobs so the filter pill reflects what the user can see. For
|
|
208
|
+
// backend-seeded buckets, the authoritative counts already won during
|
|
209
|
+
// pass 1; we only refill when the all-status count is still zero
|
|
210
|
+
// (= legacy fallback path).
|
|
211
|
+
for (const bucket of byCollection.values()){
|
|
212
|
+
if (bucket.docCountsByStatus.all !== 0) continue;
|
|
213
|
+
const byStatus = {
|
|
214
|
+
all: new Set(),
|
|
215
|
+
pending: new Set(),
|
|
216
|
+
running: new Set(),
|
|
217
|
+
completed: new Set(),
|
|
218
|
+
failed: new Set(),
|
|
219
|
+
skipped: new Set(),
|
|
220
|
+
reverted: new Set()
|
|
221
|
+
};
|
|
222
|
+
for (const doc of bucket.docs.values()){
|
|
223
|
+
byStatus.all.add(doc.documentId);
|
|
224
|
+
for (const job of doc.jobs){
|
|
225
|
+
if (job.status === 'success') byStatus.completed.add(doc.documentId);
|
|
226
|
+
else if (job.status === 'failed') byStatus.failed.add(doc.documentId);
|
|
227
|
+
else if (job.status === 'pending') byStatus.pending.add(doc.documentId);
|
|
228
|
+
else if (job.status === 'running') byStatus.running.add(doc.documentId);
|
|
229
|
+
else if (job.status === 'skipped') byStatus.skipped.add(doc.documentId);
|
|
230
|
+
else if (job.status === 'reverted') byStatus.reverted.add(doc.documentId);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
bucket.docCountsByStatus = {
|
|
234
|
+
all: byStatus.all.size,
|
|
235
|
+
pending: byStatus.pending.size,
|
|
236
|
+
running: byStatus.running.size,
|
|
237
|
+
completed: byStatus.completed.size,
|
|
238
|
+
failed: byStatus.failed.size,
|
|
239
|
+
skipped: byStatus.skipped.size,
|
|
240
|
+
reverted: byStatus.reverted.size
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// For fallback buckets (no backend counts), totalDocs is at least
|
|
244
|
+
// loadedDocs so the "Showing X of Y" hint doesn't false-trigger.
|
|
245
|
+
// Also compute the per-doc span here — only doable once we've seen
|
|
246
|
+
// every locale for the doc.
|
|
247
|
+
for (const bucket of byCollection.values()){
|
|
248
|
+
if (bucket.totalDocs < bucket.loadedDocs) {
|
|
249
|
+
bucket.totalDocs = bucket.loadedDocs;
|
|
250
|
+
}
|
|
251
|
+
for (const doc of bucket.docs.values()){
|
|
252
|
+
let minStartedAt = null;
|
|
253
|
+
let maxCompletedAt = null;
|
|
254
|
+
for (const job of doc.jobs){
|
|
255
|
+
if (job.startedAt) {
|
|
256
|
+
const ms = new Date(job.startedAt).getTime();
|
|
257
|
+
if (Number.isFinite(ms)) {
|
|
258
|
+
if (minStartedAt === null || ms < minStartedAt) minStartedAt = ms;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (job.completedAt) {
|
|
262
|
+
const ms = new Date(job.completedAt).getTime();
|
|
263
|
+
if (Number.isFinite(ms)) {
|
|
264
|
+
if (maxCompletedAt === null || ms > maxCompletedAt) maxCompletedAt = ms;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (minStartedAt !== null && maxCompletedAt !== null) {
|
|
269
|
+
doc.docSpanMs = Math.max(0, maxCompletedAt - minStartedAt);
|
|
270
|
+
if (doc.docAiActiveMs !== null) {
|
|
271
|
+
doc.docQueueWaitMs = Math.max(0, doc.docSpanMs - doc.docAiActiveMs);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return [
|
|
277
|
+
...byCollection.values()
|
|
278
|
+
].sort((a, b)=>{
|
|
279
|
+
if (a.failedCount > 0 && b.failedCount === 0) return -1;
|
|
280
|
+
if (a.failedCount === 0 && b.failedCount > 0) return 1;
|
|
281
|
+
if (a.failedCount !== b.failedCount) {
|
|
282
|
+
return b.failedCount - a.failedCount;
|
|
283
|
+
}
|
|
284
|
+
if (a.isGlobal !== b.isGlobal) return a.isGlobal ? 1 : -1;
|
|
285
|
+
return a.collection.localeCompare(b.collection);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Whether a bucket should start expanded on initial render. Per UX
|
|
290
|
+
* recommendation: failed buckets expanded by default (problem is on
|
|
291
|
+
* screen without clicking), all-success buckets collapsed (don't
|
|
292
|
+
* clutter the triage view).
|
|
293
|
+
*/ export function shouldBucketBeExpandedByDefault(bucket) {
|
|
294
|
+
return bucket.failedCount > 0 || bucket.runningCount > 0;
|
|
295
|
+
}
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Bucket-header runtime label helpers — extracted so the BucketRow
|
|
298
|
+
// component can render them consistently and so they can be unit-tested.
|
|
299
|
+
// All three deal with the same edge case: the server-aggregated
|
|
300
|
+
// `aiActiveMs` sums per-unit processing time, which can exceed
|
|
301
|
+
// `collectionSpanMs` when units ran in parallel across workers. The
|
|
302
|
+
// server clamps `queueWaitMs` to `max(0, span − ai)` but doesn't touch
|
|
303
|
+
// the raw AI number; the display layer is responsible for keeping the
|
|
304
|
+
// AI / span / queue-wait numbers internally consistent. See NEW-11.
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
/**
|
|
307
|
+
* Compact duration helper used by `fmtBucketAiActive` /
|
|
308
|
+
* `fmtBucketQueued`. Thin wrapper around the shared `formatDuration`
|
|
309
|
+
* helper — kept as a separate export so the bucket-grouping module
|
|
310
|
+
* doesn't need to import from `views/shared/format.ts` directly at
|
|
311
|
+
* every call site, and so the bucket-runtime tests keep their
|
|
312
|
+
* historical name. NEW-18 (v1.2.6): carries minutes → hours → days
|
|
313
|
+
* via the shared helper; the legacy implementation capped at minutes
|
|
314
|
+
* (`964m 6s`).
|
|
315
|
+
*/ export function fmtBucketDurationShort(ms) {
|
|
316
|
+
return formatDuration(ms);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Render the bucket header "AI active" cell. When `aiActiveMs` exceeds
|
|
320
|
+
* `collectionSpanMs` (parallelism), present as `≈<span>` rather than a
|
|
321
|
+
* number that visually contradicts the total wallclock. The raw numbers
|
|
322
|
+
* are unchanged; the tooltip still shows both.
|
|
323
|
+
*/ export function fmtBucketAiActive(bucket) {
|
|
324
|
+
const ai = bucket.aiActiveMs;
|
|
325
|
+
const span = bucket.collectionSpanMs;
|
|
326
|
+
if (ai == null) return '—';
|
|
327
|
+
if (span != null && ai > span) {
|
|
328
|
+
return `≈${fmtBucketDurationShort(span)}`;
|
|
329
|
+
}
|
|
330
|
+
return fmtBucketDurationShort(ai);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Render the bucket header "Queued" cell. Uses the server-clamped queue
|
|
334
|
+
* wait but renders `0` as `'0s'` (instead of `'—'`) so editors can tell
|
|
335
|
+
* "no wait" from "data missing." When AI overran span, the queue wait
|
|
336
|
+
* is necessarily zero — surface that explicitly.
|
|
337
|
+
*/ export function fmtBucketQueued(bucket) {
|
|
338
|
+
if (bucket.aiActiveMs != null && bucket.collectionSpanMs != null && bucket.aiActiveMs > bucket.collectionSpanMs) {
|
|
339
|
+
return '0s';
|
|
340
|
+
}
|
|
341
|
+
if (bucket.queueWaitMs == null) return '—';
|
|
342
|
+
if (bucket.queueWaitMs === 0) return '0s';
|
|
343
|
+
return fmtBucketDurationShort(bucket.queueWaitMs);
|
|
344
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type FailureCodeCount = {
|
|
2
|
+
code: string;
|
|
3
|
+
count: number;
|
|
4
|
+
};
|
|
5
|
+
export type BucketFailureSummaryEntry = {
|
|
6
|
+
title: string;
|
|
7
|
+
count: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function summarizeBucketFailureTypes(topFailureCodes: ReadonlyArray<FailureCodeCount>, batchStatus: string | null | undefined): BucketFailureSummaryEntry[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helper for the BucketRow bucket-summary "X different error types"
|
|
3
|
+
* line (ROUND3-8). Extracted so the grouping logic — which routes the
|
|
4
|
+
* persisted `failureCode`s through `categorizeFailure` and then sums by
|
|
5
|
+
* the friendly title — has a unit-test pin without needing to mount the
|
|
6
|
+
* React component.
|
|
7
|
+
*
|
|
8
|
+
* Before this helper, BucketRow looked up each raw failureCode via
|
|
9
|
+
* `editorCodeFromFailureCode` directly and rendered titles like
|
|
10
|
+
* "Translation system is not set up" for cancel-mid-flight units while
|
|
11
|
+
* the unit-level drill-down (which uses `categorizeFailure` with
|
|
12
|
+
* `batchStatus` threaded through) correctly said "Stopped mid-run".
|
|
13
|
+
* Editors who didn't drill in saw a misleading config-broken signal.
|
|
14
|
+
*
|
|
15
|
+
* Two raw codes can collapse to the same friendly bucket post-
|
|
16
|
+
* categorisation (e.g. when the parent is cancelled, every code maps
|
|
17
|
+
* to `cancelled-mid-flight`). We group by the rendered title so we
|
|
18
|
+
* don't double-count.
|
|
19
|
+
*/ import { categorizeFailure, editorMessageFor } from '../../lib/error-messages.js';
|
|
20
|
+
export function summarizeBucketFailureTypes(topFailureCodes, batchStatus) {
|
|
21
|
+
const groupedCounts = new Map();
|
|
22
|
+
for (const tf of topFailureCodes){
|
|
23
|
+
const categorized = categorizeFailure({
|
|
24
|
+
failureCode: tf.code,
|
|
25
|
+
batchStatus: batchStatus ?? null
|
|
26
|
+
});
|
|
27
|
+
const title = editorMessageFor(categorized.editorCode).title;
|
|
28
|
+
groupedCounts.set(title, (groupedCounts.get(title) ?? 0) + tf.count);
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
...groupedCounts.entries()
|
|
32
|
+
].map(([title, count])=>({
|
|
33
|
+
title,
|
|
34
|
+
count
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function dedupedStatusFetch(url: string): Promise<Response>;
|
|
2
|
+
/** Test-only: reset the dedup window between tests. */
|
|
3
|
+
export declare function __resetBatchStatusInFlightForTests(): void;
|
|
4
|
+
/** Test-only: snapshot of currently in-flight URLs. */
|
|
5
|
+
export declare function __getBatchStatusInFlightUrlsForTests(): string[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ROUND3-6: Module-level dedup for the per-batch
|
|
3
|
+
* `/api/translation-hub/bulk-translate/{id}/status` fetch.
|
|
4
|
+
*
|
|
5
|
+
* Under React 18 StrictMode dev the DrillDown effect runs → cleanup →
|
|
6
|
+
* effect-again, and the first run's in-flight `fetch` doesn't abort
|
|
7
|
+
* cleanly (the request has already left the wire). The result was a
|
|
8
|
+
* paired-burst pattern on every row-expand — same pathology as NEW-5
|
|
9
|
+
* (active-poller) and ROUND2-4 (usage-summary) but on the per-batch
|
|
10
|
+
* status endpoint, which Group G's `useBulkTranslateActive` singleton
|
|
11
|
+
* doesn't cover.
|
|
12
|
+
*
|
|
13
|
+
* The map below stores the in-flight `Promise<Response>` keyed by URL.
|
|
14
|
+
* The second concurrent caller re-uses the first one instead of firing
|
|
15
|
+
* again. The dedup window is "concurrent calls during the same render
|
|
16
|
+
* flush", not "cache forever" — entries clear the instant the promise
|
|
17
|
+
* settles.
|
|
18
|
+
*/ const inFlightStatusRequests = new Map();
|
|
19
|
+
export function dedupedStatusFetch(url) {
|
|
20
|
+
const existing = inFlightStatusRequests.get(url);
|
|
21
|
+
if (existing) {
|
|
22
|
+
// Hand back a fresh clone so each caller can read the body without
|
|
23
|
+
// racing on the single underlying ReadableStream.
|
|
24
|
+
return existing.then((res)=>res.clone());
|
|
25
|
+
}
|
|
26
|
+
const p = fetch(url, {
|
|
27
|
+
credentials: 'include'
|
|
28
|
+
});
|
|
29
|
+
inFlightStatusRequests.set(url, p);
|
|
30
|
+
// Clear the in-flight entry once settled, success or failure.
|
|
31
|
+
p.finally(()=>{
|
|
32
|
+
if (inFlightStatusRequests.get(url) === p) {
|
|
33
|
+
inFlightStatusRequests.delete(url);
|
|
34
|
+
}
|
|
35
|
+
}).catch(()=>undefined);
|
|
36
|
+
return p.then((res)=>res.clone());
|
|
37
|
+
}
|
|
38
|
+
/** Test-only: reset the dedup window between tests. */ export function __resetBatchStatusInFlightForTests() {
|
|
39
|
+
inFlightStatusRequests.clear();
|
|
40
|
+
}
|
|
41
|
+
/** Test-only: snapshot of currently in-flight URLs. */ export function __getBatchStatusInFlightUrlsForTests() {
|
|
42
|
+
return [
|
|
43
|
+
...inFlightStatusRequests.keys()
|
|
44
|
+
];
|
|
45
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AdminViewServerProps } from 'payload';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Server entry for the custom `/admin/translation/runs` view.
|
|
5
|
+
*
|
|
6
|
+
* Wraps the client UI in Payload's `<DefaultTemplate>` so the page
|
|
7
|
+
* inherits the standard admin chrome (sidebar nav, top bar, breadcrumbs).
|
|
8
|
+
*
|
|
9
|
+
* Auth (1.2.8): admin and editor roles. Pre-1.2.8 was admin-only.
|
|
10
|
+
* Threaded role + identity into the client so the run list can:
|
|
11
|
+
* 1. Render an attribution chip on every row (`by alice@…`).
|
|
12
|
+
* 2. Hide the action menu (cancel / retry / revert) on rows the
|
|
13
|
+
* current viewer doesn't own — only admins or the original
|
|
14
|
+
* triggering editor can mutate a run.
|
|
15
|
+
*/
|
|
16
|
+
export declare const BulkRunsHubView: React.FC<AdminViewServerProps>;
|
|
17
|
+
export default BulkRunsHubView;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DefaultTemplate } from '@payloadcms/next/templates';
|
|
3
|
+
// Self-import via the package's external subpath — keeps the "use client"
|
|
4
|
+
// directive intact at runtime. Mirrors the same pattern in
|
|
5
|
+
// `views/TranslationHub/index.tsx`.
|
|
6
|
+
import { BulkRunsHubClient } from '@purposeinplay/payload-ai-translate/views-client';
|
|
7
|
+
import { redirect } from 'next/navigation';
|
|
8
|
+
import { formatAdminURL } from 'payload/shared';
|
|
9
|
+
/**
|
|
10
|
+
* Server entry for the custom `/admin/translation/runs` view.
|
|
11
|
+
*
|
|
12
|
+
* Wraps the client UI in Payload's `<DefaultTemplate>` so the page
|
|
13
|
+
* inherits the standard admin chrome (sidebar nav, top bar, breadcrumbs).
|
|
14
|
+
*
|
|
15
|
+
* Auth (1.2.8): admin and editor roles. Pre-1.2.8 was admin-only.
|
|
16
|
+
* Threaded role + identity into the client so the run list can:
|
|
17
|
+
* 1. Render an attribution chip on every row (`by alice@…`).
|
|
18
|
+
* 2. Hide the action menu (cancel / retry / revert) on rows the
|
|
19
|
+
* current viewer doesn't own — only admins or the original
|
|
20
|
+
* triggering editor can mutate a run.
|
|
21
|
+
*/ export const BulkRunsHubView = ({ initPageResult, params, searchParams })=>{
|
|
22
|
+
const { req, visibleEntities, permissions } = initPageResult;
|
|
23
|
+
// Payload renders CUSTOM admin views as PUBLIC by default — unlike
|
|
24
|
+
// built-in collection/global routes, which redirect unauthenticated
|
|
25
|
+
// visitors to login. Without this check, anonymous visitors got the
|
|
26
|
+
// full admin shell (nav with every collection/global name) plus this
|
|
27
|
+
// view's role message. Mirror the built-in behaviour: no user → login,
|
|
28
|
+
// preserving the deep link via ?redirect=.
|
|
29
|
+
if (!req.user) {
|
|
30
|
+
const adminRoute = req.payload.config.routes?.admin ?? '/admin';
|
|
31
|
+
redirect(formatAdminURL({
|
|
32
|
+
adminRoute,
|
|
33
|
+
path: `/login?redirect=${encodeURIComponent(`${adminRoute}/translation-runs`)}`
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
const user = req.user;
|
|
37
|
+
const roles = user?.roles ?? [];
|
|
38
|
+
const isAdmin = roles.includes('admin');
|
|
39
|
+
const isEditor = roles.includes('editor');
|
|
40
|
+
const isPermitted = isAdmin || isEditor;
|
|
41
|
+
const role = isAdmin ? 'admin' : 'editor';
|
|
42
|
+
return /*#__PURE__*/ _jsx(DefaultTemplate, {
|
|
43
|
+
i18n: req.i18n,
|
|
44
|
+
locale: req.locale,
|
|
45
|
+
params: params,
|
|
46
|
+
payload: req.payload,
|
|
47
|
+
permissions: permissions,
|
|
48
|
+
searchParams: searchParams,
|
|
49
|
+
user: req.user ?? undefined,
|
|
50
|
+
visibleEntities: visibleEntities,
|
|
51
|
+
children: isPermitted ? /*#__PURE__*/ _jsx(BulkRunsHubClient, {
|
|
52
|
+
role: role,
|
|
53
|
+
currentUserId: user?.id != null ? String(user.id) : null,
|
|
54
|
+
currentUserEmail: user?.email ?? null
|
|
55
|
+
}) : /*#__PURE__*/ _jsxs("div", {
|
|
56
|
+
style: {
|
|
57
|
+
padding: '2rem',
|
|
58
|
+
maxWidth: '600px',
|
|
59
|
+
margin: '4rem auto',
|
|
60
|
+
textAlign: 'center'
|
|
61
|
+
},
|
|
62
|
+
children: [
|
|
63
|
+
/*#__PURE__*/ _jsx("h1", {
|
|
64
|
+
style: {
|
|
65
|
+
fontSize: '1.5rem',
|
|
66
|
+
color: 'var(--theme-elevation-1000)'
|
|
67
|
+
},
|
|
68
|
+
children: "Translation Runs"
|
|
69
|
+
}),
|
|
70
|
+
/*#__PURE__*/ _jsx("p", {
|
|
71
|
+
style: {
|
|
72
|
+
color: 'var(--theme-elevation-700)'
|
|
73
|
+
},
|
|
74
|
+
children: "You need an admin or editor role to access bulk translation history."
|
|
75
|
+
})
|
|
76
|
+
]
|
|
77
|
+
})
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
export default BulkRunsHubView;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure serializers for the BulkRunsHub URL-state contract. v1.2.6
|
|
3
|
+
* BugIndex BR-7. Kept in a dependency-free module so the unit tests
|
|
4
|
+
* can exercise them without dragging Next.js's `next/navigation` (the
|
|
5
|
+
* test environment is node, not jsdom).
|
|
6
|
+
*
|
|
7
|
+
* The `useUrlFilters` hook is a thin shim that reads from the router,
|
|
8
|
+
* delegates to these helpers, and writes back via `router.replace`.
|
|
9
|
+
*/
|
|
10
|
+
import type { BulkRunsFilterState, BulkRunsTimeRange } from '../TranslationHub/BulkTranslate.types.js';
|
|
11
|
+
export declare const DEFAULT_FILTERS: BulkRunsFilterState;
|
|
12
|
+
export declare function readTimeRange(params: URLSearchParams): BulkRunsTimeRange;
|
|
13
|
+
export declare function readFilters(params: URLSearchParams): BulkRunsFilterState;
|
|
14
|
+
export declare function buildSearchString(filters: BulkRunsFilterState, timeRange: BulkRunsTimeRange): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure serializers for the BulkRunsHub URL-state contract. v1.2.6
|
|
3
|
+
* BugIndex BR-7. Kept in a dependency-free module so the unit tests
|
|
4
|
+
* can exercise them without dragging Next.js's `next/navigation` (the
|
|
5
|
+
* test environment is node, not jsdom).
|
|
6
|
+
*
|
|
7
|
+
* The `useUrlFilters` hook is a thin shim that reads from the router,
|
|
8
|
+
* delegates to these helpers, and writes back via `router.replace`.
|
|
9
|
+
*/ export const DEFAULT_FILTERS = {
|
|
10
|
+
status: '',
|
|
11
|
+
mode: '',
|
|
12
|
+
triggeredBy: '',
|
|
13
|
+
since: '',
|
|
14
|
+
until: '',
|
|
15
|
+
hasFailures: false
|
|
16
|
+
};
|
|
17
|
+
const VALID_TIME_RANGES = new Set([
|
|
18
|
+
'7d',
|
|
19
|
+
'30d',
|
|
20
|
+
'all'
|
|
21
|
+
]);
|
|
22
|
+
export function readTimeRange(params) {
|
|
23
|
+
const t = params.get('timeRange');
|
|
24
|
+
if (t && VALID_TIME_RANGES.has(t)) {
|
|
25
|
+
return t;
|
|
26
|
+
}
|
|
27
|
+
return '7d';
|
|
28
|
+
}
|
|
29
|
+
export function readFilters(params) {
|
|
30
|
+
return {
|
|
31
|
+
status: params.get('status') ?? '',
|
|
32
|
+
mode: params.get('mode') ?? '',
|
|
33
|
+
triggeredBy: params.get('triggeredBy') ?? '',
|
|
34
|
+
since: params.get('since') ?? '',
|
|
35
|
+
until: params.get('until') ?? '',
|
|
36
|
+
hasFailures: params.get('hasFailures') === 'true'
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function buildSearchString(filters, timeRange) {
|
|
40
|
+
const params = new URLSearchParams();
|
|
41
|
+
if (filters.status) params.set('status', filters.status);
|
|
42
|
+
if (filters.mode) params.set('mode', filters.mode);
|
|
43
|
+
if (filters.triggeredBy) params.set('triggeredBy', filters.triggeredBy);
|
|
44
|
+
if (filters.since) params.set('since', filters.since);
|
|
45
|
+
if (filters.until) params.set('until', filters.until);
|
|
46
|
+
if (filters.hasFailures) params.set('hasFailures', 'true');
|
|
47
|
+
if (timeRange !== '7d') params.set('timeRange', timeRange);
|
|
48
|
+
const qs = params.toString();
|
|
49
|
+
return qs ? `?${qs}` : '';
|
|
50
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BulkRunsFilterState, BulkTranslateListResponse } from '../TranslationHub/BulkTranslate.types.js';
|
|
2
|
+
export interface UseBulkRunsListResult {
|
|
3
|
+
/**
|
|
4
|
+
* Latest successful response. `null` during the initial load or when
|
|
5
|
+
* a 404 is received.
|
|
6
|
+
*/
|
|
7
|
+
data: BulkTranslateListResponse | null;
|
|
8
|
+
/** True only during the first request — subsequent polls are silent. */
|
|
9
|
+
loading: boolean;
|
|
10
|
+
/** Last fetch error. Reset to `null` on next success. */
|
|
11
|
+
error: string | null;
|
|
12
|
+
/** True after `OFFLINE_THRESHOLD` consecutive failures. */
|
|
13
|
+
isOffline: boolean;
|
|
14
|
+
/** Force an immediate re-fetch (e.g. after a cancel/retry/revert action). */
|
|
15
|
+
refetch: () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Load the next page and merge into `data.batches`. No-op when
|
|
18
|
+
* `data.nextCursor` is null.
|
|
19
|
+
*/
|
|
20
|
+
loadMore: () => Promise<void>;
|
|
21
|
+
/** True while a loadMore request is in flight. */
|
|
22
|
+
loadingMore: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function useBulkRunsList(basePath: string, filters: BulkRunsFilterState): UseBulkRunsListResult;
|
|
25
|
+
export declare const BULK_RUNS_OFFLINE_THRESHOLD = 3;
|
|
26
|
+
export declare const BULK_RUNS_POLL_INTERVAL_MS = 5000;
|