@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,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alert-banner grouping for HUB-1 (v1.2.6).
|
|
3
|
+
*
|
|
4
|
+
* The Hub Overview used to render every un-dismissed alert as its own
|
|
5
|
+
* banner. With 10+ "Translation failed repeatedly" rows from a single
|
|
6
|
+
* cron-stuck batch, the alert stack consumed ~6 vertical screens before
|
|
7
|
+
* any other Hub content was visible. We now group alerts that share
|
|
8
|
+
* `(alertType, collection, locale, age-bucket)`, render at most 3
|
|
9
|
+
* groups expanded with a "+N more" affordance, and offer a single
|
|
10
|
+
* "Dismiss all" bulk action.
|
|
11
|
+
*
|
|
12
|
+
* Helpers exported for unit tests:
|
|
13
|
+
*
|
|
14
|
+
* - `ageBucket(iso, now)` — bucket an alert by relative age so two
|
|
15
|
+
* bursts of failures hours apart don't fold into one group.
|
|
16
|
+
* - `groupAlerts(rows, now)` — pure reducer; returns ordered groups
|
|
17
|
+
* keyed by `(alertType | collection | locale | age-bucket)`.
|
|
18
|
+
* - `splitVisibleGroups(groups, max)` — split into the first N
|
|
19
|
+
* groups + a tail to be summarised by "+N more".
|
|
20
|
+
*
|
|
21
|
+
* The grouping is deterministic and stable: groups appear in the same
|
|
22
|
+
* order as their newest member, so a new failure surface always
|
|
23
|
+
* appears at the top.
|
|
24
|
+
*/
|
|
25
|
+
export type AlertAgeBucket = '<5m' | '5–30m' | '30m–1h' | '1–6h' | '6–24h';
|
|
26
|
+
export interface GroupableAlert {
|
|
27
|
+
id: number;
|
|
28
|
+
alertType: 'persistent-failure' | 'cost-guard-abort' | 'provider-outage';
|
|
29
|
+
collection?: string | null;
|
|
30
|
+
createdAt: string;
|
|
31
|
+
metadata?: Record<string, unknown> | null;
|
|
32
|
+
}
|
|
33
|
+
export interface AlertGroup<T extends GroupableAlert = GroupableAlert> {
|
|
34
|
+
/** Stable, deterministic key — `${alertType}|${collection}|${locale}|${bucket}`. */
|
|
35
|
+
key: string;
|
|
36
|
+
alertType: T['alertType'];
|
|
37
|
+
collection: string | null;
|
|
38
|
+
locale: string | null;
|
|
39
|
+
bucket: AlertAgeBucket;
|
|
40
|
+
/** Alerts in the group, sorted newest-first (matches input ordering convention). */
|
|
41
|
+
alerts: T[];
|
|
42
|
+
/** The most-recent createdAt across the group — used for sort. */
|
|
43
|
+
newestCreatedAt: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Bucket an alert's age into a coarse band. Buckets are chosen so that
|
|
47
|
+
* fresh and old failures stay visually separable in the banner stack
|
|
48
|
+
* (an editor seeing two distinct "5m" and "6h" groups for the same
|
|
49
|
+
* locale knows the system is currently failing AND has been for a
|
|
50
|
+
* while).
|
|
51
|
+
*/
|
|
52
|
+
export declare function ageBucket(iso: string, now?: Date): AlertAgeBucket;
|
|
53
|
+
/**
|
|
54
|
+
* Group alerts by `(alertType | collection | locale | age-bucket)`.
|
|
55
|
+
* Returns groups ordered by `newestCreatedAt` descending, so the most
|
|
56
|
+
* recent failure surface is always first in the stack — preserves the
|
|
57
|
+
* existing UX expectation that the top banner is the newest event.
|
|
58
|
+
*/
|
|
59
|
+
export declare function groupAlerts<T extends GroupableAlert>(rows: T[], now?: Date): AlertGroup<T>[];
|
|
60
|
+
/**
|
|
61
|
+
* Slice the group list into the head shown expanded vs the tail
|
|
62
|
+
* summarised as "+N more". Keep at least 1 visible group so the user
|
|
63
|
+
* always has something to read. Tail is returned as a count of groups
|
|
64
|
+
* and total alerts so the caller can render whichever fits the UI.
|
|
65
|
+
*/
|
|
66
|
+
export declare function splitVisibleGroups<T extends GroupableAlert>(groups: AlertGroup<T>[], maxVisible?: number): {
|
|
67
|
+
visible: AlertGroup<T>[];
|
|
68
|
+
hiddenGroupCount: number;
|
|
69
|
+
hiddenAlertCount: number;
|
|
70
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alert-banner grouping for HUB-1 (v1.2.6).
|
|
3
|
+
*
|
|
4
|
+
* The Hub Overview used to render every un-dismissed alert as its own
|
|
5
|
+
* banner. With 10+ "Translation failed repeatedly" rows from a single
|
|
6
|
+
* cron-stuck batch, the alert stack consumed ~6 vertical screens before
|
|
7
|
+
* any other Hub content was visible. We now group alerts that share
|
|
8
|
+
* `(alertType, collection, locale, age-bucket)`, render at most 3
|
|
9
|
+
* groups expanded with a "+N more" affordance, and offer a single
|
|
10
|
+
* "Dismiss all" bulk action.
|
|
11
|
+
*
|
|
12
|
+
* Helpers exported for unit tests:
|
|
13
|
+
*
|
|
14
|
+
* - `ageBucket(iso, now)` — bucket an alert by relative age so two
|
|
15
|
+
* bursts of failures hours apart don't fold into one group.
|
|
16
|
+
* - `groupAlerts(rows, now)` — pure reducer; returns ordered groups
|
|
17
|
+
* keyed by `(alertType | collection | locale | age-bucket)`.
|
|
18
|
+
* - `splitVisibleGroups(groups, max)` — split into the first N
|
|
19
|
+
* groups + a tail to be summarised by "+N more".
|
|
20
|
+
*
|
|
21
|
+
* The grouping is deterministic and stable: groups appear in the same
|
|
22
|
+
* order as their newest member, so a new failure surface always
|
|
23
|
+
* appears at the top.
|
|
24
|
+
*/ /**
|
|
25
|
+
* Bucket an alert's age into a coarse band. Buckets are chosen so that
|
|
26
|
+
* fresh and old failures stay visually separable in the banner stack
|
|
27
|
+
* (an editor seeing two distinct "5m" and "6h" groups for the same
|
|
28
|
+
* locale knows the system is currently failing AND has been for a
|
|
29
|
+
* while).
|
|
30
|
+
*/ export function ageBucket(iso, now = new Date()) {
|
|
31
|
+
const ms = now.getTime() - new Date(iso).getTime();
|
|
32
|
+
const m = Math.max(0, Math.floor(ms / 60_000));
|
|
33
|
+
if (m < 5) return '<5m';
|
|
34
|
+
if (m < 30) return '5–30m';
|
|
35
|
+
if (m < 60) return '30m–1h';
|
|
36
|
+
if (m < 6 * 60) return '1–6h';
|
|
37
|
+
return '6–24h';
|
|
38
|
+
}
|
|
39
|
+
/** Extract the locale dimension from an alert's metadata (or null). */ function localeOf(alert) {
|
|
40
|
+
const v = alert.metadata?.['locale'];
|
|
41
|
+
return typeof v === 'string' && v.length > 0 ? v : null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Group alerts by `(alertType | collection | locale | age-bucket)`.
|
|
45
|
+
* Returns groups ordered by `newestCreatedAt` descending, so the most
|
|
46
|
+
* recent failure surface is always first in the stack — preserves the
|
|
47
|
+
* existing UX expectation that the top banner is the newest event.
|
|
48
|
+
*/ export function groupAlerts(rows, now = new Date()) {
|
|
49
|
+
const map = new Map();
|
|
50
|
+
for (const row of rows){
|
|
51
|
+
const collection = row.collection ?? null;
|
|
52
|
+
const locale = localeOf(row);
|
|
53
|
+
const bucket = ageBucket(row.createdAt, now);
|
|
54
|
+
const key = `${row.alertType}|${collection ?? '∅'}|${locale ?? '∅'}|${bucket}`;
|
|
55
|
+
const existing = map.get(key);
|
|
56
|
+
if (existing) {
|
|
57
|
+
existing.alerts.push(row);
|
|
58
|
+
if (row.createdAt > existing.newestCreatedAt) {
|
|
59
|
+
existing.newestCreatedAt = row.createdAt;
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
map.set(key, {
|
|
63
|
+
key,
|
|
64
|
+
alertType: row.alertType,
|
|
65
|
+
collection,
|
|
66
|
+
locale,
|
|
67
|
+
bucket,
|
|
68
|
+
alerts: [
|
|
69
|
+
row
|
|
70
|
+
],
|
|
71
|
+
newestCreatedAt: row.createdAt
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return Array.from(map.values()).sort((a, b)=>a.newestCreatedAt < b.newestCreatedAt ? 1 : -1);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Slice the group list into the head shown expanded vs the tail
|
|
79
|
+
* summarised as "+N more". Keep at least 1 visible group so the user
|
|
80
|
+
* always has something to read. Tail is returned as a count of groups
|
|
81
|
+
* and total alerts so the caller can render whichever fits the UI.
|
|
82
|
+
*/ export function splitVisibleGroups(groups, maxVisible = 3) {
|
|
83
|
+
const safeMax = Math.max(1, maxVisible);
|
|
84
|
+
if (groups.length <= safeMax) {
|
|
85
|
+
return {
|
|
86
|
+
visible: groups,
|
|
87
|
+
hiddenGroupCount: 0,
|
|
88
|
+
hiddenAlertCount: 0
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const visible = groups.slice(0, safeMax);
|
|
92
|
+
const hidden = groups.slice(safeMax);
|
|
93
|
+
const hiddenAlertCount = hidden.reduce((sum, group)=>sum + group.alerts.length, 0);
|
|
94
|
+
return {
|
|
95
|
+
visible,
|
|
96
|
+
hiddenGroupCount: hidden.length,
|
|
97
|
+
hiddenAlertCount
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AdminViewServerProps } from 'payload';
|
|
2
|
+
import type React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Server entry for the custom `/admin/translation` 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
|
+
* — without it, the custom view replaces the entire admin layout and
|
|
9
|
+
* the operator has no way to navigate back to other surfaces.
|
|
10
|
+
*
|
|
11
|
+
* Auth (1.2.8): admin and editor roles. Pre-1.2.8 the gate was
|
|
12
|
+
* admin-only (Decision #31). Editors get a scoped variant of the Hub
|
|
13
|
+
* — own bulk runs in the Recent translations table, count-based KPI
|
|
14
|
+
* cards (no $ / tokens), Configuration / Audit & Cost / Advanced
|
|
15
|
+
* tabs hidden. Non-admin-non-editor users still see the friendly
|
|
16
|
+
* Access Denied page. The role is threaded into the client tree so
|
|
17
|
+
* rendering can adapt without a second roundtrip.
|
|
18
|
+
*/
|
|
19
|
+
export declare const TranslationHubView: React.FC<AdminViewServerProps>;
|
|
20
|
+
export default TranslationHubView;
|
|
@@ -0,0 +1,109 @@
|
|
|
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 — see tsup.config.ts comment above the
|
|
5
|
+
// `ai-translate-views-client` entry. Inlining `./Hub.client` would strip
|
|
6
|
+
// the directive during bundling and break `useState` in production.
|
|
7
|
+
import { TranslationHubClient } from '@purposeinplay/payload-ai-translate/views-client';
|
|
8
|
+
import { redirect } from 'next/navigation';
|
|
9
|
+
import { formatAdminURL } from 'payload/shared';
|
|
10
|
+
/**
|
|
11
|
+
* Server entry for the custom `/admin/translation` view.
|
|
12
|
+
*
|
|
13
|
+
* Wraps the client UI in Payload's `<DefaultTemplate>` so the page
|
|
14
|
+
* inherits the standard admin chrome (sidebar nav, top bar, breadcrumbs)
|
|
15
|
+
* — without it, the custom view replaces the entire admin layout and
|
|
16
|
+
* the operator has no way to navigate back to other surfaces.
|
|
17
|
+
*
|
|
18
|
+
* Auth (1.2.8): admin and editor roles. Pre-1.2.8 the gate was
|
|
19
|
+
* admin-only (Decision #31). Editors get a scoped variant of the Hub
|
|
20
|
+
* — own bulk runs in the Recent translations table, count-based KPI
|
|
21
|
+
* cards (no $ / tokens), Configuration / Audit & Cost / Advanced
|
|
22
|
+
* tabs hidden. Non-admin-non-editor users still see the friendly
|
|
23
|
+
* Access Denied page. The role is threaded into the client tree so
|
|
24
|
+
* rendering can adapt without a second roundtrip.
|
|
25
|
+
*/ export const TranslationHubView = ({ initPageResult, params, searchParams })=>{
|
|
26
|
+
const { req, visibleEntities, permissions } = initPageResult;
|
|
27
|
+
// Payload renders CUSTOM admin views as PUBLIC by default — unlike
|
|
28
|
+
// built-in collection/global routes, which redirect unauthenticated
|
|
29
|
+
// visitors to login. Without this check, anonymous visitors got the
|
|
30
|
+
// full admin shell (nav with every collection/global name) plus this
|
|
31
|
+
// view's role message. Mirror the built-in behaviour: no user → login,
|
|
32
|
+
// preserving the deep link via ?redirect=.
|
|
33
|
+
if (!req.user) {
|
|
34
|
+
const adminRoute = req.payload.config.routes?.admin ?? '/admin';
|
|
35
|
+
redirect(formatAdminURL({
|
|
36
|
+
adminRoute,
|
|
37
|
+
path: `/login?redirect=${encodeURIComponent(`${adminRoute}/translation`)}`
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
const user = req.user;
|
|
41
|
+
const roles = user?.roles ?? [];
|
|
42
|
+
const isAdmin = roles.includes('admin');
|
|
43
|
+
const isEditor = roles.includes('editor');
|
|
44
|
+
const isPermitted = isAdmin || isEditor;
|
|
45
|
+
const role = isAdmin ? 'admin' : 'editor';
|
|
46
|
+
return /*#__PURE__*/ _jsx(DefaultTemplate, {
|
|
47
|
+
i18n: req.i18n,
|
|
48
|
+
locale: req.locale,
|
|
49
|
+
params: params,
|
|
50
|
+
payload: req.payload,
|
|
51
|
+
permissions: permissions,
|
|
52
|
+
searchParams: searchParams,
|
|
53
|
+
user: req.user ?? undefined,
|
|
54
|
+
visibleEntities: visibleEntities,
|
|
55
|
+
children: isPermitted ? /*#__PURE__*/ _jsx(TranslationHubClient, {
|
|
56
|
+
role: role,
|
|
57
|
+
currentUserId: user?.id != null ? String(user.id) : null,
|
|
58
|
+
currentUserEmail: user?.email ?? null,
|
|
59
|
+
locales: req.payload.config.localization ? {
|
|
60
|
+
defaultLocale: req.payload.config.localization.defaultLocale,
|
|
61
|
+
locales: req.payload.config.localization.locales.map((l)=>{
|
|
62
|
+
if (typeof l === 'string') {
|
|
63
|
+
return {
|
|
64
|
+
code: l,
|
|
65
|
+
label: l
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const rawLabel = l.label;
|
|
69
|
+
let label = l.code;
|
|
70
|
+
if (typeof rawLabel === 'string') {
|
|
71
|
+
label = rawLabel;
|
|
72
|
+
} else if (rawLabel) {
|
|
73
|
+
label = Object.values(rawLabel)[0] ?? l.code;
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
code: l.code,
|
|
77
|
+
label
|
|
78
|
+
};
|
|
79
|
+
})
|
|
80
|
+
} : {
|
|
81
|
+
defaultLocale: 'en',
|
|
82
|
+
locales: []
|
|
83
|
+
}
|
|
84
|
+
}) : /*#__PURE__*/ _jsxs("div", {
|
|
85
|
+
style: {
|
|
86
|
+
padding: '2rem',
|
|
87
|
+
maxWidth: '600px',
|
|
88
|
+
margin: '4rem auto',
|
|
89
|
+
textAlign: 'center'
|
|
90
|
+
},
|
|
91
|
+
children: [
|
|
92
|
+
/*#__PURE__*/ _jsx("h1", {
|
|
93
|
+
style: {
|
|
94
|
+
fontSize: '1.5rem',
|
|
95
|
+
color: 'var(--theme-elevation-1000)'
|
|
96
|
+
},
|
|
97
|
+
children: "Translation Hub"
|
|
98
|
+
}),
|
|
99
|
+
/*#__PURE__*/ _jsx("p", {
|
|
100
|
+
style: {
|
|
101
|
+
color: 'var(--theme-elevation-700)'
|
|
102
|
+
},
|
|
103
|
+
children: "You need an admin or editor role to access translation settings."
|
|
104
|
+
})
|
|
105
|
+
]
|
|
106
|
+
})
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
export default TranslationHubView;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub tab navigation helpers.
|
|
3
|
+
*
|
|
4
|
+
* NEW-4 (v1.2.6): the Hub tab strip rendered as plain `<button>`
|
|
5
|
+
* elements — no `role="tablist"`, no `role="tab"`, no
|
|
6
|
+
* `aria-selected`, no arrow-key navigation. Screen readers heard four
|
|
7
|
+
* generic buttons; the active tab was conveyed only by a visual
|
|
8
|
+
* `border-bottom`.
|
|
9
|
+
*
|
|
10
|
+
* This module owns the WAI-ARIA Authoring Practices tabs pattern's
|
|
11
|
+
* pure parts: the id naming helpers (tab id / panel id) and the
|
|
12
|
+
* keyboard-driven next-index resolver. The Hub.client component pulls
|
|
13
|
+
* them in, wires the actual ARIA attributes on the DOM nodes, and
|
|
14
|
+
* holds the focus refs (which are inherently a DOM concern).
|
|
15
|
+
*
|
|
16
|
+
* Tested in isolation in __tests__/tabNavigation.test.ts.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Result of a keyboard event applied to the tab strip. `null` means
|
|
20
|
+
* the key was unhandled — the consumer should let the event bubble
|
|
21
|
+
* (e.g. Tab to leave the tablist, Shift+Tab, etc.).
|
|
22
|
+
*/
|
|
23
|
+
export type TabKeyAction = {
|
|
24
|
+
kind: 'move';
|
|
25
|
+
nextIndex: number;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'ignore';
|
|
28
|
+
} | null;
|
|
29
|
+
/**
|
|
30
|
+
* Apply the WAI-ARIA Authoring Practices keyboard model for a
|
|
31
|
+
* horizontal tablist with wrap:
|
|
32
|
+
* - ArrowLeft → previous tab (wraps to last)
|
|
33
|
+
* - ArrowRight → next tab (wraps to first)
|
|
34
|
+
* - Home → first tab
|
|
35
|
+
* - End → last tab
|
|
36
|
+
*
|
|
37
|
+
* Returns the next focused tab index, or `null` if the key is not one
|
|
38
|
+
* of the four navigation keys. `count` must be > 0 — caller ensures
|
|
39
|
+
* the tablist is non-empty before invoking.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveTabKey(key: string, currentIndex: number, count: number): TabKeyAction;
|
|
42
|
+
/**
|
|
43
|
+
* DOM id for the `<button role="tab">` that controls a given panel.
|
|
44
|
+
* Used by the panel via `aria-labelledby` so the tab's label voices
|
|
45
|
+
* the panel for screen readers.
|
|
46
|
+
*/
|
|
47
|
+
export declare function tabIdFor(tab: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* DOM id for the panel a tab controls. Used by the tab via
|
|
50
|
+
* `aria-controls` so AT can pair the two when the user activates the
|
|
51
|
+
* tab.
|
|
52
|
+
*/
|
|
53
|
+
export declare function panelIdFor(tab: string): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub tab navigation helpers.
|
|
3
|
+
*
|
|
4
|
+
* NEW-4 (v1.2.6): the Hub tab strip rendered as plain `<button>`
|
|
5
|
+
* elements — no `role="tablist"`, no `role="tab"`, no
|
|
6
|
+
* `aria-selected`, no arrow-key navigation. Screen readers heard four
|
|
7
|
+
* generic buttons; the active tab was conveyed only by a visual
|
|
8
|
+
* `border-bottom`.
|
|
9
|
+
*
|
|
10
|
+
* This module owns the WAI-ARIA Authoring Practices tabs pattern's
|
|
11
|
+
* pure parts: the id naming helpers (tab id / panel id) and the
|
|
12
|
+
* keyboard-driven next-index resolver. The Hub.client component pulls
|
|
13
|
+
* them in, wires the actual ARIA attributes on the DOM nodes, and
|
|
14
|
+
* holds the focus refs (which are inherently a DOM concern).
|
|
15
|
+
*
|
|
16
|
+
* Tested in isolation in __tests__/tabNavigation.test.ts.
|
|
17
|
+
*/ /**
|
|
18
|
+
* Result of a keyboard event applied to the tab strip. `null` means
|
|
19
|
+
* the key was unhandled — the consumer should let the event bubble
|
|
20
|
+
* (e.g. Tab to leave the tablist, Shift+Tab, etc.).
|
|
21
|
+
*/ /**
|
|
22
|
+
* Apply the WAI-ARIA Authoring Practices keyboard model for a
|
|
23
|
+
* horizontal tablist with wrap:
|
|
24
|
+
* - ArrowLeft → previous tab (wraps to last)
|
|
25
|
+
* - ArrowRight → next tab (wraps to first)
|
|
26
|
+
* - Home → first tab
|
|
27
|
+
* - End → last tab
|
|
28
|
+
*
|
|
29
|
+
* Returns the next focused tab index, or `null` if the key is not one
|
|
30
|
+
* of the four navigation keys. `count` must be > 0 — caller ensures
|
|
31
|
+
* the tablist is non-empty before invoking.
|
|
32
|
+
*/ export function resolveTabKey(key, currentIndex, count) {
|
|
33
|
+
if (count <= 0) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
switch(key){
|
|
37
|
+
case 'ArrowLeft':
|
|
38
|
+
return {
|
|
39
|
+
kind: 'move',
|
|
40
|
+
nextIndex: (currentIndex - 1 + count) % count
|
|
41
|
+
};
|
|
42
|
+
case 'ArrowRight':
|
|
43
|
+
return {
|
|
44
|
+
kind: 'move',
|
|
45
|
+
nextIndex: (currentIndex + 1) % count
|
|
46
|
+
};
|
|
47
|
+
case 'Home':
|
|
48
|
+
return {
|
|
49
|
+
kind: 'move',
|
|
50
|
+
nextIndex: 0
|
|
51
|
+
};
|
|
52
|
+
case 'End':
|
|
53
|
+
return {
|
|
54
|
+
kind: 'move',
|
|
55
|
+
nextIndex: count - 1
|
|
56
|
+
};
|
|
57
|
+
default:
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* DOM id for the `<button role="tab">` that controls a given panel.
|
|
63
|
+
* Used by the panel via `aria-labelledby` so the tab's label voices
|
|
64
|
+
* the panel for screen readers.
|
|
65
|
+
*/ export function tabIdFor(tab) {
|
|
66
|
+
return `translation-hub-tab-${tab}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* DOM id for the panel a tab controls. Used by the tab via
|
|
70
|
+
* `aria-controls` so AT can pair the two when the user activates the
|
|
71
|
+
* tab.
|
|
72
|
+
*/ export function panelIdFor(tab) {
|
|
73
|
+
return `translation-hub-panel-${tab}`;
|
|
74
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** 1h freshness — long enough that the editor sees the run after lunch
|
|
2
|
+
* but short enough that an 8h-old "Revert this batch" affordance isn't
|
|
3
|
+
* one accidental click away. */
|
|
4
|
+
export declare const TERMINAL_BANNER_FRESHNESS_MS: number;
|
|
5
|
+
/**
|
|
6
|
+
* Decide whether the terminal banner should still be shown for a batch
|
|
7
|
+
* based on its `completedAt` timestamp and the current time. Returns
|
|
8
|
+
* false for batches with no `completedAt` (defensive — a terminal-status
|
|
9
|
+
* batch should always have one, but we don't want to render a stale
|
|
10
|
+
* card if the field is missing).
|
|
11
|
+
*/
|
|
12
|
+
export declare function isTerminalBannerFresh(completedAt: string | null | undefined, now?: Date, freshnessMs?: number): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Read the dismissed-batch-id set from localStorage. Returns an empty
|
|
15
|
+
* set on SSR / quota / parse failure.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readDismissedBatchIds(): Set<string>;
|
|
18
|
+
/**
|
|
19
|
+
* Persist the dismissed-batch-id set to localStorage. Bounds the
|
|
20
|
+
* persisted list to the last 50 ids so a power user dismissing dozens
|
|
21
|
+
* of runs over time doesn't grow the entry indefinitely. Swallows any
|
|
22
|
+
* write failures (quota, private-browsing).
|
|
23
|
+
*/
|
|
24
|
+
export declare function writeDismissedBatchIds(ids: Set<string>): void;
|
|
25
|
+
/**
|
|
26
|
+
* Hook: returns `{ shouldShow, dismiss }` for the terminal banner of a
|
|
27
|
+
* given batch. `shouldShow` is true only when the batch is fresh
|
|
28
|
+
* (≤ 1h past completedAt) AND not in the dismissed set.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useTerminalBannerVisibility(batchId: string | null, completedAt: string | null | undefined): {
|
|
31
|
+
shouldShow: boolean;
|
|
32
|
+
dismiss: () => void;
|
|
33
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-card freshness + dismissal logic.
|
|
3
|
+
*
|
|
4
|
+
* HUB-3 (v1.2.6): the "Bulk run complete · [Revert this batch]" banner
|
|
5
|
+
* used to persist indefinitely. A batch that completed 8 hours ago kept
|
|
6
|
+
* the destructive Revert button exposed on the Hub Overview. Two fixes
|
|
7
|
+
* applied here:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Auto-hide after 1 hour past `completedAt`.** After 60 minutes
|
|
10
|
+
* the editor has either acked the run or moved on; the row remains
|
|
11
|
+
* reachable from `/admin/translation-runs` for the full 24h revert
|
|
12
|
+
* window. The banner itself just stops being a sticky top-of-Hub
|
|
13
|
+
* element.
|
|
14
|
+
* 2. **Dismiss-and-remember per batch id, per user.** The dismissed
|
|
15
|
+
* list lives in localStorage. Dismissing a batch hides it forever
|
|
16
|
+
* on that browser, including across reloads — the original
|
|
17
|
+
* symptom that prompted the report.
|
|
18
|
+
*
|
|
19
|
+
* The helpers are pure / SSR-safe and exported for unit tests. The
|
|
20
|
+
* `useTerminalBannerVisibility` hook is the integration glue.
|
|
21
|
+
*/ import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
22
|
+
const STORAGE_KEY = 'cms-plugins.ai-translate.terminalBannerDismissed';
|
|
23
|
+
/** 1h freshness — long enough that the editor sees the run after lunch
|
|
24
|
+
* but short enough that an 8h-old "Revert this batch" affordance isn't
|
|
25
|
+
* one accidental click away. */ export const TERMINAL_BANNER_FRESHNESS_MS = 60 * 60 * 1000;
|
|
26
|
+
/**
|
|
27
|
+
* Decide whether the terminal banner should still be shown for a batch
|
|
28
|
+
* based on its `completedAt` timestamp and the current time. Returns
|
|
29
|
+
* false for batches with no `completedAt` (defensive — a terminal-status
|
|
30
|
+
* batch should always have one, but we don't want to render a stale
|
|
31
|
+
* card if the field is missing).
|
|
32
|
+
*/ export function isTerminalBannerFresh(completedAt, now = new Date(), freshnessMs = TERMINAL_BANNER_FRESHNESS_MS) {
|
|
33
|
+
if (!completedAt) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const completedMs = new Date(completedAt).getTime();
|
|
37
|
+
if (!Number.isFinite(completedMs)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return now.getTime() - completedMs <= freshnessMs;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Read the dismissed-batch-id set from localStorage. Returns an empty
|
|
44
|
+
* set on SSR / quota / parse failure.
|
|
45
|
+
*/ export function readDismissedBatchIds() {
|
|
46
|
+
if (typeof window === 'undefined') {
|
|
47
|
+
return new Set();
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const raw = window.localStorage.getItem(STORAGE_KEY);
|
|
51
|
+
if (!raw) {
|
|
52
|
+
return new Set();
|
|
53
|
+
}
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
if (!Array.isArray(parsed)) {
|
|
56
|
+
return new Set();
|
|
57
|
+
}
|
|
58
|
+
return new Set(parsed.filter((v)=>typeof v === 'string'));
|
|
59
|
+
} catch {
|
|
60
|
+
return new Set();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Persist the dismissed-batch-id set to localStorage. Bounds the
|
|
65
|
+
* persisted list to the last 50 ids so a power user dismissing dozens
|
|
66
|
+
* of runs over time doesn't grow the entry indefinitely. Swallows any
|
|
67
|
+
* write failures (quota, private-browsing).
|
|
68
|
+
*/ export function writeDismissedBatchIds(ids) {
|
|
69
|
+
if (typeof window === 'undefined') {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const list = Array.from(ids).slice(-50);
|
|
74
|
+
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
|
|
75
|
+
} catch {
|
|
76
|
+
// best-effort — quota / private-browsing failures are non-fatal.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Hook: returns `{ shouldShow, dismiss }` for the terminal banner of a
|
|
81
|
+
* given batch. `shouldShow` is true only when the batch is fresh
|
|
82
|
+
* (≤ 1h past completedAt) AND not in the dismissed set.
|
|
83
|
+
*/ export function useTerminalBannerVisibility(batchId, completedAt) {
|
|
84
|
+
const [dismissedIds, setDismissedIds] = useState(()=>new Set());
|
|
85
|
+
// Defer localStorage read to mount so SSR renders deterministically
|
|
86
|
+
// (server can't see localStorage; client hydrates with the persisted
|
|
87
|
+
// set on first effect).
|
|
88
|
+
useEffect(()=>{
|
|
89
|
+
setDismissedIds(readDismissedBatchIds());
|
|
90
|
+
}, []);
|
|
91
|
+
const dismiss = useCallback(()=>{
|
|
92
|
+
if (!batchId) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
setDismissedIds((prev)=>{
|
|
96
|
+
if (prev.has(batchId)) {
|
|
97
|
+
return prev;
|
|
98
|
+
}
|
|
99
|
+
const next = new Set(prev);
|
|
100
|
+
next.add(batchId);
|
|
101
|
+
writeDismissedBatchIds(next);
|
|
102
|
+
return next;
|
|
103
|
+
});
|
|
104
|
+
}, [
|
|
105
|
+
batchId
|
|
106
|
+
]);
|
|
107
|
+
const shouldShow = useMemo(()=>{
|
|
108
|
+
if (!batchId) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (dismissedIds.has(batchId)) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
return isTerminalBannerFresh(completedAt ?? null);
|
|
115
|
+
}, [
|
|
116
|
+
batchId,
|
|
117
|
+
completedAt,
|
|
118
|
+
dismissedIds
|
|
119
|
+
]);
|
|
120
|
+
return {
|
|
121
|
+
shouldShow,
|
|
122
|
+
dismiss
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { BulkTranslateActiveResponse } from './BulkTranslate.types.js';
|
|
2
|
+
interface UseBulkTranslateActiveResult {
|
|
3
|
+
/**
|
|
4
|
+
* The most recent successful response. `null` while loading or after
|
|
5
|
+
* a 404. Distinct from the `loading` flag so consumers can render a
|
|
6
|
+
* skeleton only on the very first request.
|
|
7
|
+
*/
|
|
8
|
+
data: BulkTranslateActiveResponse | null;
|
|
9
|
+
/** True only during the first fetch. Subsequent polls update silently. */
|
|
10
|
+
loading: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Latest error message after a network/server failure. Reset to
|
|
13
|
+
* `null` on the next successful response. The 404 -> empty-batch
|
|
14
|
+
* shortcut does NOT populate this field.
|
|
15
|
+
*/
|
|
16
|
+
error: string | null;
|
|
17
|
+
/** True after `OFFLINE_AFTER_FAILED_POLLS` consecutive failures. */
|
|
18
|
+
isOffline: boolean;
|
|
19
|
+
/** Forces an immediate poll. Used by the offline-banner retry button. */
|
|
20
|
+
refetch: () => void;
|
|
21
|
+
}
|
|
22
|
+
interface PollState {
|
|
23
|
+
data: BulkTranslateActiveResponse | null;
|
|
24
|
+
loading: boolean;
|
|
25
|
+
error: string | null;
|
|
26
|
+
failedPollStreak: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Acquire a subscription slot for `basePath`. Increments the ref-count
|
|
30
|
+
* and starts polling on first subscriber. Returns an unsubscribe
|
|
31
|
+
* function that decrements + stops polling when the last subscriber
|
|
32
|
+
* unmounts. Exported for the test fixture; consumers should call
|
|
33
|
+
* `useBulkTranslateActive`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function subscribeToBulkActive(basePath: string, listener: () => void): () => void;
|
|
36
|
+
/** Test-only: read the current snapshot for a base path. */
|
|
37
|
+
export declare function getBulkActiveSnapshot(basePath: string): PollState;
|
|
38
|
+
/** Test-only: reset the registry between tests. */
|
|
39
|
+
export declare function __resetBulkActiveRegistryForTests(): void;
|
|
40
|
+
/** Test-only: snapshot of refcounts per base path. */
|
|
41
|
+
export declare function __getBulkActiveRefCountsForTests(): Record<string, number>;
|
|
42
|
+
export declare function useBulkTranslateActive(basePath: string): UseBulkTranslateActiveResult;
|
|
43
|
+
/**
|
|
44
|
+
* Exported for tests so they can compare against the same threshold the
|
|
45
|
+
* hook uses internally.
|
|
46
|
+
*/
|
|
47
|
+
export declare const BULK_TRANSLATE_OFFLINE_THRESHOLD = 3;
|
|
48
|
+
export declare const BULK_TRANSLATE_POLL_INTERVAL_MS = 5000;
|
|
49
|
+
export {};
|