@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,553 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useConfig, useField, useFormFields } from '@payloadcms/ui';
|
|
4
|
+
import { formatAdminURL } from 'payload/shared';
|
|
5
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
6
|
+
// Lightweight module-level cache of the client-config response. The
|
|
7
|
+
// component re-mounts on every row collapse/expand in the array UI,
|
|
8
|
+
// and fetching the same payload N times per page render is a waste —
|
|
9
|
+
// every row in `perCollection` needs the SAME map. TTL is intentionally
|
|
10
|
+
// long (5 min) because the map only changes when a developer adds/
|
|
11
|
+
// removes localized fields and restarts the server; admins editing the
|
|
12
|
+
// settings global don't affect it.
|
|
13
|
+
const CLIENT_CONFIG_TTL_MS = 5 * 60 * 1000;
|
|
14
|
+
let cached = null;
|
|
15
|
+
let inFlight = null;
|
|
16
|
+
async function fetchTranslatableFieldsBySlug(url) {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
if (cached && cached.expiresAt > now) return cached.value;
|
|
19
|
+
if (inFlight) return inFlight;
|
|
20
|
+
inFlight = (async ()=>{
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(url, {
|
|
23
|
+
credentials: 'include'
|
|
24
|
+
});
|
|
25
|
+
if (!res.ok) return {};
|
|
26
|
+
const data = await res.json();
|
|
27
|
+
const map = data.translatableFieldsBySlug ?? {};
|
|
28
|
+
cached = {
|
|
29
|
+
value: map,
|
|
30
|
+
expiresAt: Date.now() + CLIENT_CONFIG_TTL_MS
|
|
31
|
+
};
|
|
32
|
+
return map;
|
|
33
|
+
} catch {
|
|
34
|
+
// Don't cache failures — let the next mount retry.
|
|
35
|
+
return {};
|
|
36
|
+
} finally{
|
|
37
|
+
inFlight = null;
|
|
38
|
+
}
|
|
39
|
+
})();
|
|
40
|
+
return inFlight;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Custom admin component for `perCollection[i].excludedFieldPaths`.
|
|
44
|
+
*
|
|
45
|
+
* Stores an exclude-list (top-level field paths the admin opted OUT of
|
|
46
|
+
* translation). Rendered as a grouped list — one checkbox per
|
|
47
|
+
* top-level field on the surface. Unchecking a top-level field
|
|
48
|
+
* excludes EVERY descendant path under it (prefix-match in
|
|
49
|
+
* `isPathExcluded`).
|
|
50
|
+
*
|
|
51
|
+
* Design rationale:
|
|
52
|
+
*
|
|
53
|
+
* 1. **Editor mental model.** Editors think in terms of the fields
|
|
54
|
+
* they see in the doc editor ("Layout", "Meta"), not internal dot-
|
|
55
|
+
* paths. The earlier flat list of paths like
|
|
56
|
+
* `layout.columns.link.label` was technically correct but useless
|
|
57
|
+
* for non-developers.
|
|
58
|
+
* 2. **One checkbox per top-level field.** Coarse but predictable:
|
|
59
|
+
* "uncheck Layout" → nothing under `layout.*` is translated.
|
|
60
|
+
* Per-leaf exclusions are still expressible via direct DB writes
|
|
61
|
+
* or future per-field controls if needed; the common case is
|
|
62
|
+
* coverage at the section level.
|
|
63
|
+
* 3. **Disclosure of details.** A collapsible "Show 14 sub-paths"
|
|
64
|
+
* affordance reveals every descendant path the resolver found, so
|
|
65
|
+
* a developer / careful editor can verify what's actually
|
|
66
|
+
* affected. Read-only — not interactive.
|
|
67
|
+
* 4. **Exclude-list semantics.** Default = empty array = translate
|
|
68
|
+
* everything. New localized fields added later start "checked"
|
|
69
|
+
* automatically.
|
|
70
|
+
*
|
|
71
|
+
* Sibling slug derivation: Payload doesn't give the component direct
|
|
72
|
+
* access to its row neighbours, so we strip the trailing
|
|
73
|
+
* `.excludedFieldPaths` segment from `path` and append `.slug` to
|
|
74
|
+
* address the sibling.
|
|
75
|
+
*/ export function ExcludedFieldsField({ path }) {
|
|
76
|
+
// `text hasMany` value is `string[]` (or null/undefined when unset).
|
|
77
|
+
const { value, setValue } = useField({
|
|
78
|
+
path
|
|
79
|
+
});
|
|
80
|
+
// Respect both `routes.api` AND Next.js `basePath` (e.g. blog-wild
|
|
81
|
+
// mounts everything under `/blog`, so the api lives at
|
|
82
|
+
// `/blog/api/...`). `formatAdminURL` reads `NEXT_BASE_PATH` and
|
|
83
|
+
// prepends it; using a raw fetch with `apiRoute` alone 404s in
|
|
84
|
+
// basePath setups.
|
|
85
|
+
const { config } = useConfig();
|
|
86
|
+
const apiRoute = config.routes?.api ?? '/api';
|
|
87
|
+
const endpointUrl = useMemo(()=>formatAdminURL({
|
|
88
|
+
apiRoute,
|
|
89
|
+
path: '/ai-translate/client-config',
|
|
90
|
+
serverURL: ''
|
|
91
|
+
}), [
|
|
92
|
+
apiRoute
|
|
93
|
+
]);
|
|
94
|
+
// Sibling `slug` value, read from the form state. `useFormFields`
|
|
95
|
+
// re-renders when the selected value changes — so the widget
|
|
96
|
+
// updates as soon as the admin picks a slug from the dropdown.
|
|
97
|
+
const siblingSlugPath = useMemo(()=>path.replace(/\.excludedFieldPaths$/, '.slug'), [
|
|
98
|
+
path
|
|
99
|
+
]);
|
|
100
|
+
const slug = useFormFields(([fields])=>fields?.[siblingSlugPath]?.value);
|
|
101
|
+
const [map, setMap] = useState(null);
|
|
102
|
+
const [loading, setLoading] = useState(true);
|
|
103
|
+
useEffect(()=>{
|
|
104
|
+
let alive = true;
|
|
105
|
+
fetchTranslatableFieldsBySlug(endpointUrl).then((m)=>{
|
|
106
|
+
if (alive) {
|
|
107
|
+
setMap(m);
|
|
108
|
+
setLoading(false);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return ()=>{
|
|
112
|
+
alive = false;
|
|
113
|
+
};
|
|
114
|
+
}, [
|
|
115
|
+
endpointUrl
|
|
116
|
+
]);
|
|
117
|
+
const availablePaths = useMemo(()=>{
|
|
118
|
+
if (!slug || !map) return null;
|
|
119
|
+
return map[slug] ?? [];
|
|
120
|
+
}, [
|
|
121
|
+
slug,
|
|
122
|
+
map
|
|
123
|
+
]);
|
|
124
|
+
// Group paths by their first segment. Each group becomes a single
|
|
125
|
+
// checkbox row — that's the editor's unit of control. Children
|
|
126
|
+
// surface in the disclosure under each group for transparency.
|
|
127
|
+
const groups = useMemo(()=>{
|
|
128
|
+
if (!availablePaths) return null;
|
|
129
|
+
const byTop = new Map();
|
|
130
|
+
for (const p of availablePaths){
|
|
131
|
+
const top = p.split('.')[0];
|
|
132
|
+
const list = byTop.get(top) ?? [];
|
|
133
|
+
list.push(p);
|
|
134
|
+
byTop.set(top, list);
|
|
135
|
+
}
|
|
136
|
+
return [
|
|
137
|
+
...byTop.entries()
|
|
138
|
+
].sort(([a], [b])=>a.localeCompare(b)).map(([top, paths])=>({
|
|
139
|
+
top,
|
|
140
|
+
paths
|
|
141
|
+
}));
|
|
142
|
+
}, [
|
|
143
|
+
availablePaths
|
|
144
|
+
]);
|
|
145
|
+
const excluded = useMemo(()=>new Set(Array.isArray(value) ? value.filter((v)=>typeof v === 'string') : []), [
|
|
146
|
+
value
|
|
147
|
+
]);
|
|
148
|
+
// Top-level checkbox: storing the parent path (e.g. `layout`) is all
|
|
149
|
+
// we need — the API filter does prefix matching, so excluding
|
|
150
|
+
// `layout` covers `layout.columns.richText` etc. automatically.
|
|
151
|
+
//
|
|
152
|
+
// BUG-06 fix: pre-fix, adding a parent path silently DELETED any
|
|
153
|
+
// descendant entries the admin had specifically configured. Toggling
|
|
154
|
+
// the parent off then permanently destroyed that fine-grained
|
|
155
|
+
// intent. Now we PRESERVE descendants — the parent's prefix-match
|
|
156
|
+
// is the primary effect, but descendant entries stay in storage so
|
|
157
|
+
// unchecking the parent restores the previous specific state.
|
|
158
|
+
const handleToggleTop = useCallback((topPath)=>{
|
|
159
|
+
const next = new Set(excluded);
|
|
160
|
+
if (next.has(topPath)) {
|
|
161
|
+
next.delete(topPath);
|
|
162
|
+
} else {
|
|
163
|
+
next.add(topPath);
|
|
164
|
+
// BUG-06: do NOT delete descendants. Prefix-match makes them
|
|
165
|
+
// redundant for the filter's purposes, but the entries are
|
|
166
|
+
// the admin's recorded intent. Keeping them lets unchecking
|
|
167
|
+
// the parent reveal the original fine-grained state.
|
|
168
|
+
}
|
|
169
|
+
// Normalize empty -> null so the DB row keeps an empty array
|
|
170
|
+
// instead of an explicit-empty marker. Both are semantically
|
|
171
|
+
// identical downstream, but `null` round-trips cleaner.
|
|
172
|
+
setValue(next.size === 0 ? null : [
|
|
173
|
+
...next
|
|
174
|
+
]);
|
|
175
|
+
}, [
|
|
176
|
+
excluded,
|
|
177
|
+
setValue
|
|
178
|
+
]);
|
|
179
|
+
const handleSelectAll = useCallback(()=>{
|
|
180
|
+
setValue(null);
|
|
181
|
+
}, [
|
|
182
|
+
setValue
|
|
183
|
+
]);
|
|
184
|
+
// BUG-07 fix: pre-fix, "Exclude all" REPLACED the whole set with just
|
|
185
|
+
// top-level paths, wiping any fine-grained entries the admin had
|
|
186
|
+
// configured. Now it ADDS top-level paths to the existing set —
|
|
187
|
+
// descendants survive so the original specificity is preserved.
|
|
188
|
+
const handleExcludeAll = useCallback(()=>{
|
|
189
|
+
if (!groups || groups.length === 0) return;
|
|
190
|
+
const next = new Set(excluded);
|
|
191
|
+
for (const g of groups)next.add(g.top);
|
|
192
|
+
setValue(next.size === 0 ? null : [
|
|
193
|
+
...next
|
|
194
|
+
]);
|
|
195
|
+
}, [
|
|
196
|
+
groups,
|
|
197
|
+
excluded,
|
|
198
|
+
setValue
|
|
199
|
+
]);
|
|
200
|
+
// --- Empty / loading states ---
|
|
201
|
+
if (!slug) {
|
|
202
|
+
return /*#__PURE__*/ _jsx(Wrapper, {
|
|
203
|
+
children: /*#__PURE__*/ _jsx("p", {
|
|
204
|
+
style: mutedStyle,
|
|
205
|
+
children: "Select a slug above and the translatable fields for that collection will appear here."
|
|
206
|
+
})
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (loading || !map) {
|
|
210
|
+
return /*#__PURE__*/ _jsx(Wrapper, {
|
|
211
|
+
children: /*#__PURE__*/ _jsx("p", {
|
|
212
|
+
style: mutedStyle,
|
|
213
|
+
children: "Loading available fields…"
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (!groups || groups.length === 0) {
|
|
218
|
+
return /*#__PURE__*/ _jsx(Wrapper, {
|
|
219
|
+
children: /*#__PURE__*/ _jsxs("p", {
|
|
220
|
+
style: mutedStyle,
|
|
221
|
+
children: [
|
|
222
|
+
'No translatable fields found for "',
|
|
223
|
+
slug,
|
|
224
|
+
'". A field needs',
|
|
225
|
+
' ',
|
|
226
|
+
/*#__PURE__*/ _jsx("code", {
|
|
227
|
+
style: inlineCodeStyle,
|
|
228
|
+
children: "localized: true"
|
|
229
|
+
}),
|
|
230
|
+
" in its config to appear here."
|
|
231
|
+
]
|
|
232
|
+
})
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// Stale top-level entries — excluded paths that no longer exist as
|
|
236
|
+
// top-level fields on the surface. Survive a schema rename so the
|
|
237
|
+
// admin can clean them up without DB access.
|
|
238
|
+
const knownTops = new Set(groups.map((g)=>g.top));
|
|
239
|
+
const staleEntries = [
|
|
240
|
+
...excluded
|
|
241
|
+
].filter((p)=>{
|
|
242
|
+
const top = p.split('.')[0];
|
|
243
|
+
return !knownTops.has(top);
|
|
244
|
+
});
|
|
245
|
+
// BUG-09 fix: surface deeper-than-top-level exclusions in the UI.
|
|
246
|
+
// Pre-fix, paths like `layout.columns.richText` were invisible —
|
|
247
|
+
// the widget grouped only on top segment, so admins debugging
|
|
248
|
+
// "why isn't this translating?" had no way to discover the deep
|
|
249
|
+
// entry. Now we show them explicitly under an "Advanced path
|
|
250
|
+
// exclusions" section.
|
|
251
|
+
const deepEntries = [
|
|
252
|
+
...excluded
|
|
253
|
+
].filter((p)=>{
|
|
254
|
+
const top = p.split('.')[0];
|
|
255
|
+
return knownTops.has(top) && p.includes('.');
|
|
256
|
+
});
|
|
257
|
+
const removeDeepEntry = (path)=>{
|
|
258
|
+
const next = new Set(excluded);
|
|
259
|
+
next.delete(path);
|
|
260
|
+
setValue(next.size === 0 ? null : [
|
|
261
|
+
...next
|
|
262
|
+
]);
|
|
263
|
+
};
|
|
264
|
+
return /*#__PURE__*/ _jsxs(Wrapper, {
|
|
265
|
+
children: [
|
|
266
|
+
/*#__PURE__*/ _jsxs("p", {
|
|
267
|
+
style: mutedStyle,
|
|
268
|
+
children: [
|
|
269
|
+
"Pick which top-level fields get translated for this surface. Unchecked fields are",
|
|
270
|
+
' ',
|
|
271
|
+
/*#__PURE__*/ _jsx("strong", {
|
|
272
|
+
children: "excluded from all translation"
|
|
273
|
+
}),
|
|
274
|
+
" — both auto-translate on publish and the manual Translate… dialog. Defaults to every field being translated."
|
|
275
|
+
]
|
|
276
|
+
}),
|
|
277
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
278
|
+
style: actionsRowStyle,
|
|
279
|
+
children: [
|
|
280
|
+
/*#__PURE__*/ _jsx("button", {
|
|
281
|
+
type: "button",
|
|
282
|
+
onClick: handleSelectAll,
|
|
283
|
+
style: linkButtonStyle,
|
|
284
|
+
children: "Translate all"
|
|
285
|
+
}),
|
|
286
|
+
/*#__PURE__*/ _jsx("span", {
|
|
287
|
+
style: dividerStyle,
|
|
288
|
+
"aria-hidden": true,
|
|
289
|
+
children: "·"
|
|
290
|
+
}),
|
|
291
|
+
/*#__PURE__*/ _jsx("button", {
|
|
292
|
+
type: "button",
|
|
293
|
+
onClick: handleExcludeAll,
|
|
294
|
+
style: linkButtonStyle,
|
|
295
|
+
children: "Exclude all"
|
|
296
|
+
})
|
|
297
|
+
]
|
|
298
|
+
}),
|
|
299
|
+
/*#__PURE__*/ _jsx("ul", {
|
|
300
|
+
style: listStyle,
|
|
301
|
+
children: groups.map(({ top, paths })=>/*#__PURE__*/ _jsx(GroupRow, {
|
|
302
|
+
top: top,
|
|
303
|
+
paths: paths,
|
|
304
|
+
excluded: excluded.has(top),
|
|
305
|
+
onToggle: handleToggleTop
|
|
306
|
+
}, top))
|
|
307
|
+
}),
|
|
308
|
+
deepEntries.length > 0 && /*#__PURE__*/ _jsxs("details", {
|
|
309
|
+
style: staleDetailsStyle,
|
|
310
|
+
children: [
|
|
311
|
+
/*#__PURE__*/ _jsxs("summary", {
|
|
312
|
+
style: staleSummaryStyle,
|
|
313
|
+
children: [
|
|
314
|
+
deepEntries.length,
|
|
315
|
+
" advanced path exclusion",
|
|
316
|
+
deepEntries.length === 1 ? '' : 's',
|
|
317
|
+
" below top level (BUG-09)"
|
|
318
|
+
]
|
|
319
|
+
}),
|
|
320
|
+
/*#__PURE__*/ _jsx("p", {
|
|
321
|
+
style: mutedStyle,
|
|
322
|
+
children: "These nested paths exclude specific fields below their top-level parent. They were written via SQL, migration, or an earlier version of this widget — and remain in effect independently of the top-level checkboxes above. Remove individual entries if you want to translate them."
|
|
323
|
+
}),
|
|
324
|
+
/*#__PURE__*/ _jsx("ul", {
|
|
325
|
+
style: staleListStyle,
|
|
326
|
+
children: deepEntries.map((deep)=>/*#__PURE__*/ _jsxs("li", {
|
|
327
|
+
style: staleItemStyle,
|
|
328
|
+
children: [
|
|
329
|
+
/*#__PURE__*/ _jsx("code", {
|
|
330
|
+
style: inlineCodeStyle,
|
|
331
|
+
children: deep
|
|
332
|
+
}),
|
|
333
|
+
/*#__PURE__*/ _jsx("button", {
|
|
334
|
+
type: "button",
|
|
335
|
+
onClick: ()=>removeDeepEntry(deep),
|
|
336
|
+
style: {
|
|
337
|
+
...linkButtonStyle,
|
|
338
|
+
marginLeft: '0.75rem'
|
|
339
|
+
},
|
|
340
|
+
"aria-label": `Remove advanced exclusion ${deep}`,
|
|
341
|
+
children: "remove"
|
|
342
|
+
})
|
|
343
|
+
]
|
|
344
|
+
}, deep))
|
|
345
|
+
})
|
|
346
|
+
]
|
|
347
|
+
}),
|
|
348
|
+
staleEntries.length > 0 && /*#__PURE__*/ _jsxs("details", {
|
|
349
|
+
style: staleDetailsStyle,
|
|
350
|
+
children: [
|
|
351
|
+
/*#__PURE__*/ _jsxs("summary", {
|
|
352
|
+
style: staleSummaryStyle,
|
|
353
|
+
children: [
|
|
354
|
+
staleEntries.length,
|
|
355
|
+
" stored exclusion",
|
|
356
|
+
staleEntries.length === 1 ? '' : 's',
|
|
357
|
+
" no longer present on this surface"
|
|
358
|
+
]
|
|
359
|
+
}),
|
|
360
|
+
/*#__PURE__*/ _jsx("ul", {
|
|
361
|
+
style: staleListStyle,
|
|
362
|
+
children: staleEntries.map((stale)=>/*#__PURE__*/ _jsx("li", {
|
|
363
|
+
style: staleItemStyle,
|
|
364
|
+
children: /*#__PURE__*/ _jsx("code", {
|
|
365
|
+
style: inlineCodeStyle,
|
|
366
|
+
children: stale
|
|
367
|
+
})
|
|
368
|
+
}, stale))
|
|
369
|
+
})
|
|
370
|
+
]
|
|
371
|
+
})
|
|
372
|
+
]
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
function Wrapper({ children }) {
|
|
376
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
377
|
+
className: "field-type",
|
|
378
|
+
children: [
|
|
379
|
+
/*#__PURE__*/ _jsx("label", {
|
|
380
|
+
className: "field-label",
|
|
381
|
+
children: "Translated fields"
|
|
382
|
+
}),
|
|
383
|
+
children
|
|
384
|
+
]
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
function GroupRow({ top, paths, excluded, onToggle }) {
|
|
388
|
+
// Only show the disclosure when there's actually descendant info to
|
|
389
|
+
// disclose. A flat top-level field (e.g. `title`) has only one
|
|
390
|
+
// path — itself — and showing "1 sub-path: title" is just noise.
|
|
391
|
+
const hasChildren = paths.some((p)=>p !== top);
|
|
392
|
+
return /*#__PURE__*/ _jsxs("li", {
|
|
393
|
+
style: groupItemStyle,
|
|
394
|
+
children: [
|
|
395
|
+
/*#__PURE__*/ _jsxs("label", {
|
|
396
|
+
style: labelRowStyle,
|
|
397
|
+
children: [
|
|
398
|
+
/*#__PURE__*/ _jsx("input", {
|
|
399
|
+
type: "checkbox",
|
|
400
|
+
checked: !excluded,
|
|
401
|
+
onChange: ()=>onToggle(top)
|
|
402
|
+
}),
|
|
403
|
+
/*#__PURE__*/ _jsx("span", {
|
|
404
|
+
style: groupNameStyle,
|
|
405
|
+
children: humanize(top)
|
|
406
|
+
}),
|
|
407
|
+
/*#__PURE__*/ _jsx("code", {
|
|
408
|
+
style: inlineCodeStyle,
|
|
409
|
+
children: top
|
|
410
|
+
})
|
|
411
|
+
]
|
|
412
|
+
}),
|
|
413
|
+
hasChildren && /*#__PURE__*/ _jsxs("details", {
|
|
414
|
+
style: disclosureStyle,
|
|
415
|
+
children: [
|
|
416
|
+
/*#__PURE__*/ _jsxs("summary", {
|
|
417
|
+
style: disclosureSummaryStyle,
|
|
418
|
+
children: [
|
|
419
|
+
"Show ",
|
|
420
|
+
paths.length,
|
|
421
|
+
" field path",
|
|
422
|
+
paths.length === 1 ? '' : 's',
|
|
423
|
+
" covered"
|
|
424
|
+
]
|
|
425
|
+
}),
|
|
426
|
+
/*#__PURE__*/ _jsx("ul", {
|
|
427
|
+
style: pathListStyle,
|
|
428
|
+
children: paths.map((p)=>/*#__PURE__*/ _jsx("li", {
|
|
429
|
+
style: pathListItemStyle,
|
|
430
|
+
children: /*#__PURE__*/ _jsx("code", {
|
|
431
|
+
style: inlineCodeStyle,
|
|
432
|
+
children: p
|
|
433
|
+
})
|
|
434
|
+
}, p))
|
|
435
|
+
})
|
|
436
|
+
]
|
|
437
|
+
})
|
|
438
|
+
]
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Turn a kebab/snake/camel field name into a human label.
|
|
443
|
+
* `metaTitle` -> `Meta Title`. `social_links` -> `Social Links`.
|
|
444
|
+
* `meta-data` -> `Meta Data`.
|
|
445
|
+
*
|
|
446
|
+
* Best-effort only — Payload field labels can be arbitrary strings or
|
|
447
|
+
* functions in the field config. Exposing them through the public
|
|
448
|
+
* client-config endpoint would mean expanding the API contract; this
|
|
449
|
+
* heuristic covers 90% of cases for the common camelCase / snake_case
|
|
450
|
+
* patterns Payload generates by default.
|
|
451
|
+
*/ function humanize(name) {
|
|
452
|
+
return name.replace(/[-_]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2').replace(/\b\w/g, (c)=>c.toUpperCase());
|
|
453
|
+
}
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// Inline styles — keeps the component zero-dep on consumer CSS. Uses
|
|
456
|
+
// Payload's CSS custom properties so the widget matches the active
|
|
457
|
+
// theme (light/dark) without an additional stylesheet import.
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
const mutedStyle = {
|
|
460
|
+
color: 'var(--theme-elevation-500)',
|
|
461
|
+
fontSize: '0.85em',
|
|
462
|
+
margin: '0.25em 0 0.75em'
|
|
463
|
+
};
|
|
464
|
+
const actionsRowStyle = {
|
|
465
|
+
display: 'flex',
|
|
466
|
+
alignItems: 'center',
|
|
467
|
+
gap: '0.5em',
|
|
468
|
+
margin: '0 0 0.5em',
|
|
469
|
+
fontSize: '0.85em'
|
|
470
|
+
};
|
|
471
|
+
const linkButtonStyle = {
|
|
472
|
+
background: 'none',
|
|
473
|
+
border: 'none',
|
|
474
|
+
color: 'var(--theme-text)',
|
|
475
|
+
cursor: 'pointer',
|
|
476
|
+
padding: 0,
|
|
477
|
+
textDecoration: 'underline',
|
|
478
|
+
font: 'inherit'
|
|
479
|
+
};
|
|
480
|
+
const dividerStyle = {
|
|
481
|
+
color: 'var(--theme-elevation-400)'
|
|
482
|
+
};
|
|
483
|
+
const listStyle = {
|
|
484
|
+
listStyle: 'none',
|
|
485
|
+
margin: 0,
|
|
486
|
+
padding: '0.5em',
|
|
487
|
+
border: '1px solid var(--theme-elevation-150)',
|
|
488
|
+
borderRadius: '4px',
|
|
489
|
+
maxHeight: '420px',
|
|
490
|
+
overflowY: 'auto'
|
|
491
|
+
};
|
|
492
|
+
const groupItemStyle = {
|
|
493
|
+
padding: '0.4em 0',
|
|
494
|
+
borderBottom: '1px solid var(--theme-elevation-100)'
|
|
495
|
+
};
|
|
496
|
+
const labelRowStyle = {
|
|
497
|
+
display: 'flex',
|
|
498
|
+
alignItems: 'center',
|
|
499
|
+
gap: '0.5em',
|
|
500
|
+
cursor: 'pointer'
|
|
501
|
+
};
|
|
502
|
+
const groupNameStyle = {
|
|
503
|
+
fontWeight: 500
|
|
504
|
+
};
|
|
505
|
+
const inlineCodeStyle = {
|
|
506
|
+
fontFamily: 'var(--font-mono, monospace)',
|
|
507
|
+
fontSize: '0.8em',
|
|
508
|
+
color: 'var(--theme-elevation-500)',
|
|
509
|
+
background: 'var(--theme-elevation-100)',
|
|
510
|
+
padding: '0.05em 0.4em',
|
|
511
|
+
borderRadius: '3px'
|
|
512
|
+
};
|
|
513
|
+
const disclosureStyle = {
|
|
514
|
+
marginTop: '0.3em',
|
|
515
|
+
marginLeft: '1.6em',
|
|
516
|
+
fontSize: '0.85em'
|
|
517
|
+
};
|
|
518
|
+
const disclosureSummaryStyle = {
|
|
519
|
+
cursor: 'pointer',
|
|
520
|
+
color: 'var(--theme-elevation-500)',
|
|
521
|
+
userSelect: 'none'
|
|
522
|
+
};
|
|
523
|
+
const pathListStyle = {
|
|
524
|
+
listStyle: 'none',
|
|
525
|
+
margin: '0.3em 0 0',
|
|
526
|
+
padding: 0,
|
|
527
|
+
display: 'flex',
|
|
528
|
+
flexDirection: 'column',
|
|
529
|
+
gap: '0.15em'
|
|
530
|
+
};
|
|
531
|
+
const pathListItemStyle = {
|
|
532
|
+
paddingLeft: '0.4em'
|
|
533
|
+
};
|
|
534
|
+
const staleDetailsStyle = {
|
|
535
|
+
marginTop: '0.75em',
|
|
536
|
+
fontSize: '0.85em'
|
|
537
|
+
};
|
|
538
|
+
const staleSummaryStyle = {
|
|
539
|
+
cursor: 'pointer',
|
|
540
|
+
color: 'var(--theme-warning-700, var(--theme-elevation-500))',
|
|
541
|
+
userSelect: 'none'
|
|
542
|
+
};
|
|
543
|
+
const staleListStyle = {
|
|
544
|
+
listStyle: 'none',
|
|
545
|
+
margin: '0.3em 0 0',
|
|
546
|
+
padding: 0,
|
|
547
|
+
display: 'flex',
|
|
548
|
+
flexDirection: 'column',
|
|
549
|
+
gap: '0.15em'
|
|
550
|
+
};
|
|
551
|
+
const staleItemStyle = {
|
|
552
|
+
paddingLeft: '1em'
|
|
553
|
+
};
|