@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.
Files changed (26) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/__tests__/components/document-locale-switcher.test.d.ts +2 -0
  3. package/dist/__tests__/components/document-locale-switcher.test.d.ts.map +1 -0
  4. package/dist/__tests__/components/document-locale-switcher.test.js +78 -0
  5. package/dist/__tests__/components/document-locale-switcher.test.js.map +1 -0
  6. package/dist/components/DocumentLocaleSwitcher.d.ts +15 -0
  7. package/dist/components/DocumentLocaleSwitcher.d.ts.map +1 -0
  8. package/dist/components/DocumentLocaleSwitcher.js +66 -0
  9. package/dist/components/DocumentLocaleSwitcher.js.map +1 -0
  10. package/dist/prenta-admin.css +1 -1
  11. package/dist/views/page-editor/EditorTopBar.d.ts +4 -1
  12. package/dist/views/page-editor/EditorTopBar.d.ts.map +1 -1
  13. package/dist/views/page-editor/EditorTopBar.js +2 -2
  14. package/dist/views/page-editor/EditorTopBar.js.map +1 -1
  15. package/dist/views/page-editor/PageSectionEditor.d.ts.map +1 -1
  16. package/dist/views/page-editor/PageSectionEditor.js +2 -1
  17. package/dist/views/page-editor/PageSectionEditor.js.map +1 -1
  18. package/dist/views/post-editor/PostSectionEditor.d.ts.map +1 -1
  19. package/dist/views/post-editor/PostSectionEditor.js +2 -1
  20. package/dist/views/post-editor/PostSectionEditor.js.map +1 -1
  21. package/package.json +4 -4
  22. package/src/__tests__/components/document-locale-switcher.test.tsx +111 -0
  23. package/src/components/DocumentLocaleSwitcher.tsx +140 -0
  24. package/src/views/page-editor/EditorTopBar.tsx +4 -0
  25. package/src/views/page-editor/PageSectionEditor.tsx +12 -0
  26. 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}