@prenta/admin 2.1.0 → 2.2.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/CHANGELOG.md +6 -0
- package/dist/__tests__/components/document-locale-switcher.test.d.ts +2 -0
- package/dist/__tests__/components/document-locale-switcher.test.d.ts.map +1 -0
- package/dist/__tests__/components/document-locale-switcher.test.js +78 -0
- package/dist/__tests__/components/document-locale-switcher.test.js.map +1 -0
- package/dist/components/DocumentLocaleSwitcher.d.ts +15 -0
- package/dist/components/DocumentLocaleSwitcher.d.ts.map +1 -0
- package/dist/components/DocumentLocaleSwitcher.js +66 -0
- package/dist/components/DocumentLocaleSwitcher.js.map +1 -0
- package/dist/prenta-admin.css +1 -1
- package/dist/views/page-editor/EditorTopBar.d.ts +4 -1
- package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
- package/dist/views/page-editor/EditorTopBar.js +2 -2
- package/dist/views/page-editor/EditorTopBar.js.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
- package/dist/views/page-editor/PageSectionEditor.js +2 -1
- package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
- package/dist/views/post-editor/PostSectionEditor.js +2 -1
- package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/components/document-locale-switcher.test.tsx +111 -0
- package/src/components/DocumentLocaleSwitcher.tsx +140 -0
- package/src/views/page-editor/EditorTopBar.tsx +4 -0
- package/src/views/page-editor/PageSectionEditor.tsx +12 -0
- package/src/views/post-editor/PostSectionEditor.tsx +12 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
|
5
|
+
import { ChevronDown, Globe } from 'lucide-react'
|
|
6
|
+
import { toast } from 'sonner'
|
|
7
|
+
import { cmsApi } from '../lib/api.js'
|
|
8
|
+
import { adminPortalContainer } from '../lib/portal-container.js'
|
|
9
|
+
import { useLocale } from './LocaleProvider.js'
|
|
10
|
+
|
|
11
|
+
interface TranslationSibling {
|
|
12
|
+
id: string
|
|
13
|
+
locale: string | null
|
|
14
|
+
status: string
|
|
15
|
+
title: string | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface TranslationList {
|
|
19
|
+
locale: string | null
|
|
20
|
+
translations: TranslationSibling[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface TranslationDraft {
|
|
24
|
+
id: string
|
|
25
|
+
locale: string
|
|
26
|
+
created: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface DocumentLocaleSwitcherProps {
|
|
30
|
+
collection: string
|
|
31
|
+
documentId: string
|
|
32
|
+
canCreate: boolean
|
|
33
|
+
editorPath: (id: string) => string
|
|
34
|
+
onNavigate: (path: string) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Switches the open document to another locale, creating a draft translation
|
|
39
|
+
* when that locale does not exist yet. Hidden when the site has fewer than
|
|
40
|
+
* two locales — the header language control is a different setting.
|
|
41
|
+
*/
|
|
42
|
+
export function DocumentLocaleSwitcher({
|
|
43
|
+
collection,
|
|
44
|
+
documentId,
|
|
45
|
+
canCreate,
|
|
46
|
+
editorPath,
|
|
47
|
+
onNavigate,
|
|
48
|
+
}: DocumentLocaleSwitcherProps) {
|
|
49
|
+
const { locales } = useLocale()
|
|
50
|
+
const enabled = locales.length > 1
|
|
51
|
+
const [currentLocale, setCurrentLocale] = useState<string | null>(null)
|
|
52
|
+
const [siblings, setSiblings] = useState<TranslationSibling[]>([])
|
|
53
|
+
const [busy, setBusy] = useState(false)
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enabled) return
|
|
57
|
+
let cancelled = false
|
|
58
|
+
cmsApi<TranslationList>(
|
|
59
|
+
`/collections/${encodeURIComponent(collection)}/${encodeURIComponent(documentId)}/translations`,
|
|
60
|
+
).then((result) => {
|
|
61
|
+
if (cancelled || !result.data) return
|
|
62
|
+
setCurrentLocale(result.data.locale)
|
|
63
|
+
setSiblings(result.data.translations)
|
|
64
|
+
})
|
|
65
|
+
return () => {
|
|
66
|
+
cancelled = true
|
|
67
|
+
}
|
|
68
|
+
}, [collection, documentId, enabled])
|
|
69
|
+
|
|
70
|
+
if (!enabled) return null
|
|
71
|
+
|
|
72
|
+
const current = locales.find((locale) => locale.code === currentLocale)
|
|
73
|
+
const label = current?.code.toUpperCase() ?? 'Locale'
|
|
74
|
+
|
|
75
|
+
async function choose(code: string) {
|
|
76
|
+
const sibling = siblings.find((row) => row.locale === code)
|
|
77
|
+
if (sibling) {
|
|
78
|
+
if (sibling.id !== documentId) onNavigate(editorPath(sibling.id))
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (!canCreate || busy) return
|
|
82
|
+
setBusy(true)
|
|
83
|
+
const result = await cmsApi<TranslationDraft>(
|
|
84
|
+
`/collections/${encodeURIComponent(collection)}/${encodeURIComponent(documentId)}/translations`,
|
|
85
|
+
{ method: 'POST', body: JSON.stringify({ locale: code }) },
|
|
86
|
+
)
|
|
87
|
+
setBusy(false)
|
|
88
|
+
if (result.error || !result.data?.id) {
|
|
89
|
+
toast.error(result.error ?? 'Could not create the translation')
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
onNavigate(editorPath(result.data.id))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<DropdownMenu.Root>
|
|
97
|
+
<DropdownMenu.Trigger asChild>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
disabled={busy}
|
|
101
|
+
aria-label={`Translations, ${current?.label ?? 'locale not set'}`}
|
|
102
|
+
title="Open or create a translation"
|
|
103
|
+
className="text-muted-foreground hover:text-foreground hover:bg-accent inline-flex shrink-0 items-center gap-1 rounded-lg px-2 py-1 text-sm disabled:cursor-not-allowed disabled:opacity-50"
|
|
104
|
+
>
|
|
105
|
+
<Globe className="h-4 w-4" aria-hidden />
|
|
106
|
+
{label}
|
|
107
|
+
<ChevronDown className="h-3.5 w-3.5" aria-hidden />
|
|
108
|
+
</button>
|
|
109
|
+
</DropdownMenu.Trigger>
|
|
110
|
+
<DropdownMenu.Portal container={adminPortalContainer()}>
|
|
111
|
+
<DropdownMenu.Content
|
|
112
|
+
align="start"
|
|
113
|
+
sideOffset={6}
|
|
114
|
+
className="border-border bg-popover z-[120] min-w-40 rounded-lg border p-1 shadow-md"
|
|
115
|
+
>
|
|
116
|
+
{locales.map((locale) => {
|
|
117
|
+
const sibling = siblings.find((row) => row.locale === locale.code)
|
|
118
|
+
const isCurrent = locale.code === currentLocale
|
|
119
|
+
const canChoose = Boolean(sibling) || canCreate
|
|
120
|
+
return (
|
|
121
|
+
<DropdownMenu.Item
|
|
122
|
+
key={locale.code}
|
|
123
|
+
disabled={!canChoose || busy}
|
|
124
|
+
onSelect={() => {
|
|
125
|
+
void choose(locale.code)
|
|
126
|
+
}}
|
|
127
|
+
className="hover:bg-accent focus:bg-accent data-[disabled]:text-muted-foreground flex cursor-pointer items-center justify-between gap-3 rounded-md px-2.5 py-1.5 text-sm outline-none data-[disabled]:cursor-not-allowed"
|
|
128
|
+
>
|
|
129
|
+
<span>{locale.label}</span>
|
|
130
|
+
<span className="text-muted-foreground text-xs">
|
|
131
|
+
{isCurrent ? 'Current' : sibling ? sibling.status : 'New draft'}
|
|
132
|
+
</span>
|
|
133
|
+
</DropdownMenu.Item>
|
|
134
|
+
)
|
|
135
|
+
})}
|
|
136
|
+
</DropdownMenu.Content>
|
|
137
|
+
</DropdownMenu.Portal>
|
|
138
|
+
</DropdownMenu.Root>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -106,6 +106,8 @@ interface EditorTopBarProps {
|
|
|
106
106
|
onRedo?: () => void
|
|
107
107
|
canUndo?: boolean
|
|
108
108
|
canRedo?: boolean
|
|
109
|
+
/** Locale switcher for this document. Hidden by the caller when unused. */
|
|
110
|
+
localeControl?: ReactNode
|
|
109
111
|
onSubmitForReview: () => void
|
|
110
112
|
onApprove: () => void
|
|
111
113
|
onRequestChanges: (note: string) => void
|
|
@@ -160,6 +162,7 @@ export function EditorTopBar({
|
|
|
160
162
|
onRedo,
|
|
161
163
|
canUndo = false,
|
|
162
164
|
canRedo = false,
|
|
165
|
+
localeControl,
|
|
163
166
|
onSubmitForReview,
|
|
164
167
|
onApprove,
|
|
165
168
|
onRequestChanges,
|
|
@@ -325,6 +328,7 @@ export function EditorTopBar({
|
|
|
325
328
|
</button>
|
|
326
329
|
</span>
|
|
327
330
|
)}
|
|
331
|
+
{localeControl ? <span className="ml-1 inline-flex shrink-0">{localeControl}</span> : null}
|
|
328
332
|
</div>
|
|
329
333
|
|
|
330
334
|
{/* Right controls */}
|
|
@@ -9,6 +9,7 @@ import { SchedulePublishDialog } from '../../components/SchedulePublishDialog.js
|
|
|
9
9
|
import { SeoEditorPane } from '../../components/seo/SeoEditorPane.js'
|
|
10
10
|
import { EditorRail, RailHeader } from '../../components/EditorRail.js'
|
|
11
11
|
import { EditorRailTabs, type RailTab, type RailTabId } from '../../components/EditorRailTabs.js'
|
|
12
|
+
import { DocumentLocaleSwitcher } from '../../components/DocumentLocaleSwitcher.js'
|
|
12
13
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
13
14
|
import { VersionHistoryPanel } from './VersionHistoryPanel.js'
|
|
14
15
|
import { useDebouncedAutosave } from './autosave.js'
|
|
@@ -993,6 +994,17 @@ export function PageSectionEditor({
|
|
|
993
994
|
onRedo={effectiveCanEdit ? handleRedo : undefined}
|
|
994
995
|
canUndo={canUndo}
|
|
995
996
|
canRedo={canRedo}
|
|
997
|
+
localeControl={
|
|
998
|
+
page.id ? (
|
|
999
|
+
<DocumentLocaleSwitcher
|
|
1000
|
+
collection={PAGES_COLLECTION}
|
|
1001
|
+
documentId={page.id}
|
|
1002
|
+
canCreate={effectiveCanEdit}
|
|
1003
|
+
editorPath={(id) => `/pages/${id}/edit`}
|
|
1004
|
+
onNavigate={guardedNavigate}
|
|
1005
|
+
/>
|
|
1006
|
+
) : null
|
|
1007
|
+
}
|
|
996
1008
|
onSubmitForReview={handleSubmitForReview}
|
|
997
1009
|
onApprove={handleApprove}
|
|
998
1010
|
onRequestChanges={handleRequestChanges}
|
|
@@ -9,6 +9,7 @@ import { SchedulePublishDialog } from '../../components/SchedulePublishDialog.js
|
|
|
9
9
|
import { EditorRail, RailHeader } from '../../components/EditorRail.js'
|
|
10
10
|
import { EditorRailTabs, type RailTab, type RailTabId } from '../../components/EditorRailTabs.js'
|
|
11
11
|
import { SeoEditorPane } from '../../components/seo/SeoEditorPane.js'
|
|
12
|
+
import { DocumentLocaleSwitcher } from '../../components/DocumentLocaleSwitcher.js'
|
|
12
13
|
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
|
|
13
14
|
import { VersionHistoryPanel } from '../page-editor/VersionHistoryPanel.js'
|
|
14
15
|
import { useDebouncedAutosave } from '../page-editor/autosave.js'
|
|
@@ -864,6 +865,17 @@ export function PostSectionEditor({
|
|
|
864
865
|
onRedo={effectiveCanEdit ? handleRedo : undefined}
|
|
865
866
|
canUndo={canUndo}
|
|
866
867
|
canRedo={canRedo}
|
|
868
|
+
localeControl={
|
|
869
|
+
post.id ? (
|
|
870
|
+
<DocumentLocaleSwitcher
|
|
871
|
+
collection={postType}
|
|
872
|
+
documentId={post.id}
|
|
873
|
+
canCreate={effectiveCanEdit}
|
|
874
|
+
editorPath={(id) => `/posts/${postType}/${id}/edit`}
|
|
875
|
+
onNavigate={guardedNavigate}
|
|
876
|
+
/>
|
|
877
|
+
) : null
|
|
878
|
+
}
|
|
867
879
|
onSubmitForReview={handleSubmitForReview}
|
|
868
880
|
onApprove={handleApprove}
|
|
869
881
|
onRequestChanges={handleRequestChanges}
|