@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.
Files changed (301) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +714 -0
  3. package/dist/alerts-collection.d.ts +21 -0
  4. package/dist/alerts-collection.js +159 -0
  5. package/dist/api.d.ts +4 -0
  6. package/dist/api.js +918 -0
  7. package/dist/bulk-translate-batches-collection.d.ts +29 -0
  8. package/dist/bulk-translate-batches-collection.js +404 -0
  9. package/dist/bulk-translate-units-collection.d.ts +35 -0
  10. package/dist/bulk-translate-units-collection.js +310 -0
  11. package/dist/client/estimated-cost-cell.d.ts +6 -0
  12. package/dist/client/estimated-cost-cell.js +12 -0
  13. package/dist/client/excluded-fields-field.d.ts +45 -0
  14. package/dist/client/excluded-fields-field.js +553 -0
  15. package/dist/client/field-translate-button.d.ts +6 -0
  16. package/dist/client/field-translate-button.js +199 -0
  17. package/dist/client/index.d.ts +6 -0
  18. package/dist/client/index.js +6 -0
  19. package/dist/client/lib/use-global-kill-switches.d.ts +20 -0
  20. package/dist/client/lib/use-global-kill-switches.js +58 -0
  21. package/dist/client/translate-button.d.ts +2 -0
  22. package/dist/client/translate-button.js +228 -0
  23. package/dist/client/translate-modal.d.ts +16 -0
  24. package/dist/client/translate-modal.js +549 -0
  25. package/dist/client/translation-progress.d.ts +10 -0
  26. package/dist/client/translation-progress.js +297 -0
  27. package/dist/components/TranslationNavGroup.d.ts +45 -0
  28. package/dist/components/TranslationNavGroup.js +104 -0
  29. package/dist/defaults.d.ts +11 -0
  30. package/dist/defaults.js +16 -0
  31. package/dist/endpoints/client-config.d.ts +44 -0
  32. package/dist/endpoints/client-config.js +145 -0
  33. package/dist/endpoints/estimate.d.ts +5 -0
  34. package/dist/endpoints/estimate.js +237 -0
  35. package/dist/endpoints/progress.d.ts +2 -0
  36. package/dist/endpoints/progress.js +314 -0
  37. package/dist/endpoints/translate.d.ts +11 -0
  38. package/dist/endpoints/translate.js +376 -0
  39. package/dist/endpoints/translation-hub/_helpers.d.ts +140 -0
  40. package/dist/endpoints/translation-hub/_helpers.js +297 -0
  41. package/dist/endpoints/translation-hub/active.d.ts +21 -0
  42. package/dist/endpoints/translation-hub/active.js +220 -0
  43. package/dist/endpoints/translation-hub/cancel.d.ts +22 -0
  44. package/dist/endpoints/translation-hub/cancel.js +233 -0
  45. package/dist/endpoints/translation-hub/enqueue.d.ts +70 -0
  46. package/dist/endpoints/translation-hub/enqueue.js +529 -0
  47. package/dist/endpoints/translation-hub/failures.d.ts +12 -0
  48. package/dist/endpoints/translation-hub/failures.js +67 -0
  49. package/dist/endpoints/translation-hub/force-reset.d.ts +20 -0
  50. package/dist/endpoints/translation-hub/force-reset.js +144 -0
  51. package/dist/endpoints/translation-hub/index.d.ts +21 -0
  52. package/dist/endpoints/translation-hub/index.js +20 -0
  53. package/dist/endpoints/translation-hub/list.d.ts +40 -0
  54. package/dist/endpoints/translation-hub/list.js +182 -0
  55. package/dist/endpoints/translation-hub/preflight.d.ts +19 -0
  56. package/dist/endpoints/translation-hub/preflight.js +141 -0
  57. package/dist/endpoints/translation-hub/retry-failed.d.ts +38 -0
  58. package/dist/endpoints/translation-hub/retry-failed.js +235 -0
  59. package/dist/endpoints/translation-hub/revert.d.ts +88 -0
  60. package/dist/endpoints/translation-hub/revert.js +405 -0
  61. package/dist/endpoints/translation-hub/status.d.ts +45 -0
  62. package/dist/endpoints/translation-hub/status.js +391 -0
  63. package/dist/endpoints/translation-hub/usage-summary.d.ts +114 -0
  64. package/dist/endpoints/translation-hub/usage-summary.js +481 -0
  65. package/dist/exports/client.d.ts +6 -0
  66. package/dist/exports/client.js +6 -0
  67. package/dist/exports/components.d.ts +6 -0
  68. package/dist/exports/components.js +5 -0
  69. package/dist/exports/index.d.ts +8 -0
  70. package/dist/exports/index.js +7 -0
  71. package/dist/exports/providers.d.ts +9 -0
  72. package/dist/exports/providers.js +5 -0
  73. package/dist/exports/views-client.d.ts +23 -0
  74. package/dist/exports/views-client.js +22 -0
  75. package/dist/exports/views.d.ts +30 -0
  76. package/dist/exports/views.js +29 -0
  77. package/dist/hooks/after-change-global.d.ts +4 -0
  78. package/dist/hooks/after-change-global.js +109 -0
  79. package/dist/hooks/after-change.d.ts +16 -0
  80. package/dist/hooks/after-change.js +205 -0
  81. package/dist/hooks/after-delete.d.ts +30 -0
  82. package/dist/hooks/after-delete.js +95 -0
  83. package/dist/index.d.ts +5 -0
  84. package/dist/index.js +5 -0
  85. package/dist/jobs-collection.d.ts +17 -0
  86. package/dist/jobs-collection.js +139 -0
  87. package/dist/lexical/classifier.d.ts +3 -0
  88. package/dist/lexical/classifier.js +108 -0
  89. package/dist/lexical/deserializer.d.ts +4 -0
  90. package/dist/lexical/deserializer.js +263 -0
  91. package/dist/lexical/placeholder-integrity.d.ts +6 -0
  92. package/dist/lexical/placeholder-integrity.js +21 -0
  93. package/dist/lexical/placeholders.d.ts +21 -0
  94. package/dist/lexical/placeholders.js +117 -0
  95. package/dist/lexical/serializer.d.ts +21 -0
  96. package/dist/lexical/serializer.js +233 -0
  97. package/dist/lexical/types.d.ts +32 -0
  98. package/dist/lexical/types.js +1 -0
  99. package/dist/lib/auth-diagnostics.d.ts +14 -0
  100. package/dist/lib/auth-diagnostics.js +19 -0
  101. package/dist/lib/batch-counts.d.ts +58 -0
  102. package/dist/lib/batch-counts.js +105 -0
  103. package/dist/lib/bulk-translate-migrations.d.ts +92 -0
  104. package/dist/lib/bulk-translate-migrations.js +153 -0
  105. package/dist/lib/coalescing-queue.d.ts +38 -0
  106. package/dist/lib/coalescing-queue.js +69 -0
  107. package/dist/lib/content-extractor.d.ts +16 -0
  108. package/dist/lib/content-extractor.js +410 -0
  109. package/dist/lib/content-hash.d.ts +1 -0
  110. package/dist/lib/content-hash.js +19 -0
  111. package/dist/lib/content-patcher.d.ts +15 -0
  112. package/dist/lib/content-patcher.js +293 -0
  113. package/dist/lib/cost-guards.d.ts +2 -0
  114. package/dist/lib/cost-guards.js +18 -0
  115. package/dist/lib/daily-spend-cap.d.ts +58 -0
  116. package/dist/lib/daily-spend-cap.js +233 -0
  117. package/dist/lib/effective-locales.d.ts +181 -0
  118. package/dist/lib/effective-locales.js +302 -0
  119. package/dist/lib/error-messages.d.ts +245 -0
  120. package/dist/lib/error-messages.js +626 -0
  121. package/dist/lib/events.d.ts +39 -0
  122. package/dist/lib/events.js +146 -0
  123. package/dist/lib/exclude-fields.d.ts +3 -0
  124. package/dist/lib/exclude-fields.js +64 -0
  125. package/dist/lib/field-breadcrumb.d.ts +31 -0
  126. package/dist/lib/field-breadcrumb.js +227 -0
  127. package/dist/lib/field-diff.d.ts +1 -0
  128. package/dist/lib/field-diff.js +25 -0
  129. package/dist/lib/field-empty.d.ts +2 -0
  130. package/dist/lib/field-empty.js +68 -0
  131. package/dist/lib/field-resolver.d.ts +3 -0
  132. package/dist/lib/field-resolver.js +164 -0
  133. package/dist/lib/group-soft-skips.d.ts +39 -0
  134. package/dist/lib/group-soft-skips.js +45 -0
  135. package/dist/lib/locale-merge.d.ts +44 -0
  136. package/dist/lib/locale-merge.js +357 -0
  137. package/dist/lib/locale-row-check.d.ts +30 -0
  138. package/dist/lib/locale-row-check.js +64 -0
  139. package/dist/lib/logger.d.ts +74 -0
  140. package/dist/lib/logger.js +97 -0
  141. package/dist/lib/manual-edit-guard.d.ts +128 -0
  142. package/dist/lib/manual-edit-guard.js +393 -0
  143. package/dist/lib/output-validation.d.ts +48 -0
  144. package/dist/lib/output-validation.js +148 -0
  145. package/dist/lib/payload-read.d.ts +16 -0
  146. package/dist/lib/payload-read.js +51 -0
  147. package/dist/lib/per-doc-claim.d.ts +90 -0
  148. package/dist/lib/per-doc-claim.js +140 -0
  149. package/dist/lib/per-doc-lock.d.ts +94 -0
  150. package/dist/lib/per-doc-lock.js +119 -0
  151. package/dist/lib/persist-usage.d.ts +91 -0
  152. package/dist/lib/persist-usage.js +116 -0
  153. package/dist/lib/progress-store.d.ts +103 -0
  154. package/dist/lib/progress-store.js +314 -0
  155. package/dist/lib/rate-limiter.d.ts +3 -0
  156. package/dist/lib/rate-limiter.js +53 -0
  157. package/dist/lib/snapshot-select.d.ts +43 -0
  158. package/dist/lib/snapshot-select.js +108 -0
  159. package/dist/lib/translate-prompt.d.ts +31 -0
  160. package/dist/lib/translate-prompt.js +66 -0
  161. package/dist/lib/translation-token-bucket.d.ts +57 -0
  162. package/dist/lib/translation-token-bucket.js +365 -0
  163. package/dist/lib/truncate-source-value.d.ts +1 -0
  164. package/dist/lib/truncate-source-value.js +27 -0
  165. package/dist/manual-edit-collection.d.ts +22 -0
  166. package/dist/manual-edit-collection.js +124 -0
  167. package/dist/plugin.d.ts +3 -0
  168. package/dist/plugin.js +934 -0
  169. package/dist/providers/ai-sdk-adapter.d.ts +35 -0
  170. package/dist/providers/ai-sdk-adapter.js +100 -0
  171. package/dist/providers/anthropic.d.ts +31 -0
  172. package/dist/providers/anthropic.js +66 -0
  173. package/dist/providers/custom.d.ts +36 -0
  174. package/dist/providers/custom.js +24 -0
  175. package/dist/providers/gemini.d.ts +20 -0
  176. package/dist/providers/gemini.js +48 -0
  177. package/dist/providers/mock.d.ts +2 -0
  178. package/dist/providers/mock.js +29 -0
  179. package/dist/providers/openai.d.ts +28 -0
  180. package/dist/providers/openai.js +69 -0
  181. package/dist/settings-global.d.ts +74 -0
  182. package/dist/settings-global.js +216 -0
  183. package/dist/tasks/bulk-translate-coordinator.d.ts +115 -0
  184. package/dist/tasks/bulk-translate-coordinator.js +708 -0
  185. package/dist/tasks/bulk-translate-doc-task.d.ts +142 -0
  186. package/dist/tasks/bulk-translate-doc-task.js +1000 -0
  187. package/dist/tasks/bulk-translate-janitor.d.ts +87 -0
  188. package/dist/tasks/bulk-translate-janitor.js +311 -0
  189. package/dist/tasks/translate-job-task.d.ts +51 -0
  190. package/dist/tasks/translate-job-task.js +154 -0
  191. package/dist/translate.d.ts +113 -0
  192. package/dist/translate.js +911 -0
  193. package/dist/translation-daily-spend-collection.d.ts +24 -0
  194. package/dist/translation-daily-spend-collection.js +133 -0
  195. package/dist/translation-rate-limits-collection.d.ts +30 -0
  196. package/dist/translation-rate-limits-collection.js +144 -0
  197. package/dist/types.d.ts +672 -0
  198. package/dist/types.js +1 -0
  199. package/dist/usage-collection.d.ts +14 -0
  200. package/dist/usage-collection.js +377 -0
  201. package/dist/views/BulkRunsHub/BatchRow.d.ts +32 -0
  202. package/dist/views/BulkRunsHub/BatchRow.js +1222 -0
  203. package/dist/views/BulkRunsHub/BucketRow.d.ts +62 -0
  204. package/dist/views/BulkRunsHub/BucketRow.js +982 -0
  205. package/dist/views/BulkRunsHub/BulkRunsHub.client.d.ts +18 -0
  206. package/dist/views/BulkRunsHub/BulkRunsHub.client.js +331 -0
  207. package/dist/views/BulkRunsHub/EmptyState.d.ts +6 -0
  208. package/dist/views/BulkRunsHub/EmptyState.js +64 -0
  209. package/dist/views/BulkRunsHub/FilterBar.d.ts +16 -0
  210. package/dist/views/BulkRunsHub/FilterBar.js +284 -0
  211. package/dist/views/BulkRunsHub/InFlightBanner.d.ts +14 -0
  212. package/dist/views/BulkRunsHub/InFlightBanner.js +59 -0
  213. package/dist/views/BulkRunsHub/StatusBadge.d.ts +64 -0
  214. package/dist/views/BulkRunsHub/StatusBadge.js +248 -0
  215. package/dist/views/BulkRunsHub/SummaryStrip.d.ts +22 -0
  216. package/dist/views/BulkRunsHub/SummaryStrip.js +249 -0
  217. package/dist/views/BulkRunsHub/bucket-grouping.d.ts +200 -0
  218. package/dist/views/BulkRunsHub/bucket-grouping.js +344 -0
  219. package/dist/views/BulkRunsHub/bucketFailureSummary.d.ts +9 -0
  220. package/dist/views/BulkRunsHub/bucketFailureSummary.js +36 -0
  221. package/dist/views/BulkRunsHub/dedupedStatusFetch.d.ts +5 -0
  222. package/dist/views/BulkRunsHub/dedupedStatusFetch.js +45 -0
  223. package/dist/views/BulkRunsHub/index.d.ts +17 -0
  224. package/dist/views/BulkRunsHub/index.js +80 -0
  225. package/dist/views/BulkRunsHub/urlFilters.d.ts +14 -0
  226. package/dist/views/BulkRunsHub/urlFilters.js +50 -0
  227. package/dist/views/BulkRunsHub/useBulkRunsList.d.ts +26 -0
  228. package/dist/views/BulkRunsHub/useBulkRunsList.js +204 -0
  229. package/dist/views/BulkRunsHub/useUrlFilters.d.ts +10 -0
  230. package/dist/views/BulkRunsHub/useUrlFilters.js +88 -0
  231. package/dist/views/TranslationHub/ActiveJobs.d.ts +6 -0
  232. package/dist/views/TranslationHub/ActiveJobs.js +320 -0
  233. package/dist/views/TranslationHub/AdvancedPanel.d.ts +17 -0
  234. package/dist/views/TranslationHub/AdvancedPanel.js +996 -0
  235. package/dist/views/TranslationHub/AlertBanner.d.ts +6 -0
  236. package/dist/views/TranslationHub/AlertBanner.js +568 -0
  237. package/dist/views/TranslationHub/AuditPanel.d.ts +6 -0
  238. package/dist/views/TranslationHub/AuditPanel.helpers.d.ts +44 -0
  239. package/dist/views/TranslationHub/AuditPanel.helpers.js +71 -0
  240. package/dist/views/TranslationHub/AuditPanel.js +1367 -0
  241. package/dist/views/TranslationHub/BulkTranslate.types.d.ts +242 -0
  242. package/dist/views/TranslationHub/BulkTranslate.types.js +36 -0
  243. package/dist/views/TranslationHub/BulkTranslateFailureDrawer.d.ts +19 -0
  244. package/dist/views/TranslationHub/BulkTranslateFailureDrawer.js +332 -0
  245. package/dist/views/TranslationHub/BulkTranslateMonitor.d.ts +28 -0
  246. package/dist/views/TranslationHub/BulkTranslateMonitor.js +305 -0
  247. package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.d.ts +3 -0
  248. package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.js +42 -0
  249. package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.d.ts +26 -0
  250. package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.js +95 -0
  251. package/dist/views/TranslationHub/BulkTranslatePreflightModal.d.ts +22 -0
  252. package/dist/views/TranslationHub/BulkTranslatePreflightModal.js +879 -0
  253. package/dist/views/TranslationHub/BulkTranslateTerminalCard.d.ts +29 -0
  254. package/dist/views/TranslationHub/BulkTranslateTerminalCard.js +445 -0
  255. package/dist/views/TranslationHub/BulkTranslateTrigger.d.ts +66 -0
  256. package/dist/views/TranslationHub/BulkTranslateTrigger.js +161 -0
  257. package/dist/views/TranslationHub/EditorRecentRunsPanel.d.ts +33 -0
  258. package/dist/views/TranslationHub/EditorRecentRunsPanel.js +290 -0
  259. package/dist/views/TranslationHub/Hub.client.d.ts +74 -0
  260. package/dist/views/TranslationHub/Hub.client.js +357 -0
  261. package/dist/views/TranslationHub/ModelCombobox.d.ts +14 -0
  262. package/dist/views/TranslationHub/ModelCombobox.js +415 -0
  263. package/dist/views/TranslationHub/PerCollectionConfig.d.ts +10 -0
  264. package/dist/views/TranslationHub/PerCollectionConfig.helpers.d.ts +16 -0
  265. package/dist/views/TranslationHub/PerCollectionConfig.helpers.js +19 -0
  266. package/dist/views/TranslationHub/PerCollectionConfig.js +759 -0
  267. package/dist/views/TranslationHub/SettingsRail.d.ts +11 -0
  268. package/dist/views/TranslationHub/SettingsRail.js +382 -0
  269. package/dist/views/TranslationHub/StatusStrip.d.ts +6 -0
  270. package/dist/views/TranslationHub/StatusStrip.js +451 -0
  271. package/dist/views/TranslationHub/UsageTable.d.ts +6 -0
  272. package/dist/views/TranslationHub/UsageTable.helpers.d.ts +69 -0
  273. package/dist/views/TranslationHub/UsageTable.helpers.js +49 -0
  274. package/dist/views/TranslationHub/UsageTable.js +1240 -0
  275. package/dist/views/TranslationHub/alertGrouping.d.ts +70 -0
  276. package/dist/views/TranslationHub/alertGrouping.js +99 -0
  277. package/dist/views/TranslationHub/index.d.ts +20 -0
  278. package/dist/views/TranslationHub/index.js +109 -0
  279. package/dist/views/TranslationHub/tabNavigation.d.ts +53 -0
  280. package/dist/views/TranslationHub/tabNavigation.js +74 -0
  281. package/dist/views/TranslationHub/terminalBannerVisibility.d.ts +33 -0
  282. package/dist/views/TranslationHub/terminalBannerVisibility.js +124 -0
  283. package/dist/views/TranslationHub/useBulkTranslateActive.d.ts +49 -0
  284. package/dist/views/TranslationHub/useBulkTranslateActive.js +251 -0
  285. package/dist/views/TranslationHub/useFocusTrap.d.ts +6 -0
  286. package/dist/views/TranslationHub/useFocusTrap.js +81 -0
  287. package/dist/views/TranslationHub/useTranslationHubUsageSummary.d.ts +77 -0
  288. package/dist/views/TranslationHub/useTranslationHubUsageSummary.js +267 -0
  289. package/dist/views/shared/EditorError.d.ts +97 -0
  290. package/dist/views/shared/EditorError.js +205 -0
  291. package/dist/views/shared/ModelCell.d.ts +18 -0
  292. package/dist/views/shared/ModelCell.js +31 -0
  293. package/dist/views/shared/docHref.d.ts +16 -0
  294. package/dist/views/shared/docHref.js +26 -0
  295. package/dist/views/shared/fetch-error-body.d.ts +25 -0
  296. package/dist/views/shared/fetch-error-body.js +42 -0
  297. package/dist/views/shared/filterPillStyle.d.ts +35 -0
  298. package/dist/views/shared/filterPillStyle.js +40 -0
  299. package/dist/views/shared/format.d.ts +75 -0
  300. package/dist/views/shared/format.js +131 -0
  301. 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;