@prenta/admin 2.1.0 → 2.3.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 +12 -0
- package/dist/__tests__/components/batch-ai-actions.test.d.ts +2 -0
- package/dist/__tests__/components/batch-ai-actions.test.d.ts.map +1 -0
- package/dist/__tests__/components/batch-ai-actions.test.js +42 -0
- package/dist/__tests__/components/batch-ai-actions.test.js.map +1 -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/BatchAiActions.d.ts +15 -0
- package/dist/components/BatchAiActions.d.ts.map +1 -0
- package/dist/components/BatchAiActions.js +47 -0
- package/dist/components/BatchAiActions.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/components/ui/Badge.d.ts +1 -1
- package/dist/components/ui/Button.d.ts +1 -1
- package/dist/prenta-admin.css +1 -1
- package/dist/views/CollectionList.d.ts.map +1 -1
- package/dist/views/CollectionList.js +2 -1
- package/dist/views/CollectionList.js.map +1 -1
- package/dist/views/Pages/PagesListView.d.ts.map +1 -1
- package/dist/views/Pages/PagesListView.js +4 -3
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Posts/PostsListView.d.ts.map +1 -1
- package/dist/views/Posts/PostsListView.js +4 -3
- package/dist/views/Posts/PostsListView.js.map +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 +3 -3
- package/src/__tests__/components/batch-ai-actions.test.tsx +52 -0
- package/src/__tests__/components/document-locale-switcher.test.tsx +111 -0
- package/src/components/BatchAiActions.tsx +83 -0
- package/src/components/DocumentLocaleSwitcher.tsx +140 -0
- package/src/views/CollectionList.tsx +6 -0
- package/src/views/Pages/PagesListView.tsx +12 -0
- package/src/views/Posts/PostsListView.tsx +8 -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,111 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { DocumentLocaleSwitcher } from '../../components/DocumentLocaleSwitcher.js'
|
|
6
|
+
import { LocaleProvider } from '../../components/LocaleProvider.js'
|
|
7
|
+
import { cmsApi } from '../../lib/api.js'
|
|
8
|
+
|
|
9
|
+
vi.mock('../../lib/api.js', () => ({
|
|
10
|
+
cmsApi: vi.fn(),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
const cmsApiMock = vi.mocked(cmsApi)
|
|
14
|
+
|
|
15
|
+
const locales = {
|
|
16
|
+
i18n: {
|
|
17
|
+
defaultLocale: 'en',
|
|
18
|
+
locales: [
|
|
19
|
+
{ code: 'en', label: 'English' },
|
|
20
|
+
{ code: 'es', label: 'Spanish' },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderSwitcher(onNavigate = vi.fn()) {
|
|
26
|
+
render(
|
|
27
|
+
<LocaleProvider config={locales}>
|
|
28
|
+
<DocumentLocaleSwitcher
|
|
29
|
+
collection="pages"
|
|
30
|
+
documentId="page-1"
|
|
31
|
+
canCreate
|
|
32
|
+
editorPath={(id) => `/pages/${id}/edit`}
|
|
33
|
+
onNavigate={onNavigate}
|
|
34
|
+
/>
|
|
35
|
+
</LocaleProvider>,
|
|
36
|
+
)
|
|
37
|
+
return onNavigate
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('DocumentLocaleSwitcher', () => {
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
cmsApiMock.mockReset()
|
|
43
|
+
cmsApiMock.mockResolvedValue({
|
|
44
|
+
data: {
|
|
45
|
+
locale: 'en',
|
|
46
|
+
translations: [{ id: 'page-1', locale: 'en', status: 'PUBLISHED', title: 'About' }],
|
|
47
|
+
},
|
|
48
|
+
status: 200,
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('hides itself when the site has a single locale', () => {
|
|
53
|
+
render(
|
|
54
|
+
<LocaleProvider
|
|
55
|
+
config={{ i18n: { defaultLocale: 'en', locales: [{ code: 'en', label: 'English' }] } }}
|
|
56
|
+
>
|
|
57
|
+
<DocumentLocaleSwitcher
|
|
58
|
+
collection="pages"
|
|
59
|
+
documentId="page-1"
|
|
60
|
+
canCreate
|
|
61
|
+
editorPath={(id) => `/pages/${id}/edit`}
|
|
62
|
+
onNavigate={vi.fn()}
|
|
63
|
+
/>
|
|
64
|
+
</LocaleProvider>,
|
|
65
|
+
)
|
|
66
|
+
expect(screen.queryByRole('button', { name: /Translations/ })).toBeNull()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('opens an existing translation', async () => {
|
|
70
|
+
cmsApiMock.mockResolvedValue({
|
|
71
|
+
data: {
|
|
72
|
+
locale: 'en',
|
|
73
|
+
translations: [
|
|
74
|
+
{ id: 'page-1', locale: 'en', status: 'PUBLISHED', title: 'About' },
|
|
75
|
+
{ id: 'page-es', locale: 'es', status: 'DRAFT', title: 'About' },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
status: 200,
|
|
79
|
+
})
|
|
80
|
+
const onNavigate = renderSwitcher()
|
|
81
|
+
await waitFor(() =>
|
|
82
|
+
expect(screen.getByRole('button', { name: 'Translations, English' })).toBeTruthy(),
|
|
83
|
+
)
|
|
84
|
+
fireEvent.keyDown(screen.getByRole('button', { name: 'Translations, English' }), {
|
|
85
|
+
key: 'Enter',
|
|
86
|
+
})
|
|
87
|
+
fireEvent.click(screen.getByRole('menuitem', { name: /Spanish/ }))
|
|
88
|
+
expect(onNavigate).toHaveBeenCalledWith('/pages/page-es/edit')
|
|
89
|
+
expect(cmsApiMock).toHaveBeenCalledTimes(1)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('creates a draft and opens it', async () => {
|
|
93
|
+
const onNavigate = renderSwitcher()
|
|
94
|
+
await waitFor(() =>
|
|
95
|
+
expect(screen.getByRole('button', { name: 'Translations, English' })).toBeTruthy(),
|
|
96
|
+
)
|
|
97
|
+
cmsApiMock.mockResolvedValueOnce({
|
|
98
|
+
data: { id: 'page-es', locale: 'es', created: true },
|
|
99
|
+
status: 201,
|
|
100
|
+
})
|
|
101
|
+
fireEvent.keyDown(screen.getByRole('button', { name: 'Translations, English' }), {
|
|
102
|
+
key: 'Enter',
|
|
103
|
+
})
|
|
104
|
+
fireEvent.click(screen.getByRole('menuitem', { name: /Spanish/ }))
|
|
105
|
+
await waitFor(() => expect(onNavigate).toHaveBeenCalledWith('/pages/page-es/edit'))
|
|
106
|
+
expect(cmsApiMock).toHaveBeenLastCalledWith('/collections/pages/page-1/translations', {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: JSON.stringify({ locale: 'es' }),
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { toast } from 'sonner'
|
|
5
|
+
import { cmsApi } from '../lib/api.js'
|
|
6
|
+
import { ConfirmDialog } from './ui/ConfirmDialog.js'
|
|
7
|
+
|
|
8
|
+
type BatchOperation = 'meta' | 'alt-text' | 'summarize'
|
|
9
|
+
|
|
10
|
+
const LABELS: Record<BatchOperation, string> = {
|
|
11
|
+
meta: 'Meta descriptions',
|
|
12
|
+
'alt-text': 'Alt text',
|
|
13
|
+
summarize: 'Summaries',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface BatchAiActionsProps {
|
|
17
|
+
documents: Array<{ collection: string; id: string }>
|
|
18
|
+
onQueued?: () => void
|
|
19
|
+
buttonClassName?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Queues a list-view AI batch after a confirm that names the daily cap.
|
|
24
|
+
* The server writes field drafts and does not publish.
|
|
25
|
+
*/
|
|
26
|
+
export function BatchAiActions({ documents, onQueued, buttonClassName }: BatchAiActionsProps) {
|
|
27
|
+
const [operation, setOperation] = useState<BatchOperation | null>(null)
|
|
28
|
+
const [busy, setBusy] = useState(false)
|
|
29
|
+
const buttonClass =
|
|
30
|
+
buttonClassName ??
|
|
31
|
+
'text-foreground hover:bg-accent rounded-md px-2 py-1 text-sm transition-colors'
|
|
32
|
+
|
|
33
|
+
async function confirm() {
|
|
34
|
+
if (!operation || busy) return
|
|
35
|
+
setBusy(true)
|
|
36
|
+
const result = await cmsApi<{ jobId: string; count: number }>('/ai/batch', {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body: JSON.stringify({ operation, documents }),
|
|
39
|
+
})
|
|
40
|
+
setBusy(false)
|
|
41
|
+
setOperation(null)
|
|
42
|
+
if (result.error || !result.data?.jobId) {
|
|
43
|
+
toast.error(result.error ?? 'Could not queue the batch')
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
toast.success(
|
|
47
|
+
`Queued ${LABELS[operation].toLowerCase()} for ${result.data.count} ${result.data.count === 1 ? 'document' : 'documents'}`,
|
|
48
|
+
)
|
|
49
|
+
onQueued?.()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
{(Object.keys(LABELS) as BatchOperation[]).map((key) => (
|
|
55
|
+
<button
|
|
56
|
+
key={key}
|
|
57
|
+
type="button"
|
|
58
|
+
disabled={busy || documents.length === 0}
|
|
59
|
+
onClick={() => setOperation(key)}
|
|
60
|
+
className={buttonClass}
|
|
61
|
+
>
|
|
62
|
+
{LABELS[key]}
|
|
63
|
+
</button>
|
|
64
|
+
))}
|
|
65
|
+
<ConfirmDialog
|
|
66
|
+
open={operation !== null}
|
|
67
|
+
onClose={() => {
|
|
68
|
+
if (!busy) setOperation(null)
|
|
69
|
+
}}
|
|
70
|
+
onConfirm={() => {
|
|
71
|
+
void confirm()
|
|
72
|
+
}}
|
|
73
|
+
title={operation ? `Write ${LABELS[operation].toLowerCase()}?` : 'Write drafts?'}
|
|
74
|
+
description={
|
|
75
|
+
operation
|
|
76
|
+
? `This queues ${LABELS[operation].toLowerCase()} for ${documents.length} selected ${documents.length === 1 ? 'document' : 'documents'}. Each document uses one request against the daily AI cap. Nothing is published.`
|
|
77
|
+
: ''
|
|
78
|
+
}
|
|
79
|
+
confirmLabel={busy ? 'Queuing…' : 'Queue drafts'}
|
|
80
|
+
/>
|
|
81
|
+
</>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from 'lucide-react'
|
|
13
13
|
import { useState, useMemo, useCallback, useEffect, useRef } from 'react'
|
|
14
14
|
import { toast } from 'sonner'
|
|
15
|
+
import { BatchAiActions } from '../components/BatchAiActions.js'
|
|
15
16
|
import { cmsApi } from '../lib/api.js'
|
|
16
17
|
import { useApiData } from '../lib/useApiData.js'
|
|
17
18
|
import { useVirtualWindow, VirtualTablePadding } from '../components/virtual-window.js'
|
|
@@ -501,6 +502,11 @@ export function CollectionList({ collectionSlug, config, onNavigate }: Collectio
|
|
|
501
502
|
>
|
|
502
503
|
Unpublish
|
|
503
504
|
</button>
|
|
505
|
+
<BatchAiActions
|
|
506
|
+
documents={[...selected].map((id) => ({ collection: collectionSlug, id }))}
|
|
507
|
+
onQueued={() => setSelected(new Set())}
|
|
508
|
+
buttonClassName="rounded-lg px-3 py-1.5 text-sm"
|
|
509
|
+
/>
|
|
504
510
|
<button
|
|
505
511
|
onClick={() => bulkAction('delete')}
|
|
506
512
|
className="rounded-lg px-3 py-1.5 text-sm text-white"
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
X,
|
|
20
20
|
} from 'lucide-react'
|
|
21
21
|
import { toast } from 'sonner'
|
|
22
|
+
import { BatchAiActions } from '../../components/BatchAiActions.js'
|
|
22
23
|
import { useVirtualWindow, VirtualTablePadding } from '../../components/virtual-window.js'
|
|
23
24
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
|
24
25
|
import { adminPortalContainer } from '../../lib/portal-container.js'
|
|
@@ -667,6 +668,8 @@ export function PagesListView({ onNavigate }: PagesListViewProps) {
|
|
|
667
668
|
onPublish={() => runBulkStatus('PUBLISHED')}
|
|
668
669
|
onDraft={() => runBulkStatus('DRAFT')}
|
|
669
670
|
onAddTag={(slug) => runBulkTag(slug, 'add')}
|
|
671
|
+
documents={selectedPages.map((page) => ({ collection: 'pages', id: page.id }))}
|
|
672
|
+
onQueued={() => setSelected(new Set())}
|
|
670
673
|
onRemoveTag={(slug) => runBulkTag(slug, 'remove')}
|
|
671
674
|
onDelete={() => setPendingDelete(true)}
|
|
672
675
|
deletableCount={deletablePages.length}
|
|
@@ -1154,6 +1157,8 @@ function BulkActionBar({
|
|
|
1154
1157
|
onAddTag,
|
|
1155
1158
|
onRemoveTag,
|
|
1156
1159
|
onDelete,
|
|
1160
|
+
documents,
|
|
1161
|
+
onQueued,
|
|
1157
1162
|
}: {
|
|
1158
1163
|
count: number
|
|
1159
1164
|
tags: PageTag[]
|
|
@@ -1165,6 +1170,8 @@ function BulkActionBar({
|
|
|
1165
1170
|
onAddTag: (slug: string) => void
|
|
1166
1171
|
onRemoveTag: (slug: string) => void
|
|
1167
1172
|
onDelete: () => void
|
|
1173
|
+
documents: Array<{ collection: string; id: string }>
|
|
1174
|
+
onQueued: () => void
|
|
1168
1175
|
}) {
|
|
1169
1176
|
return (
|
|
1170
1177
|
<div className="border-primary/30 bg-primary/5 flex flex-wrap items-center gap-2 rounded-lg border px-4 py-2">
|
|
@@ -1186,6 +1193,11 @@ function BulkActionBar({
|
|
|
1186
1193
|
</button>
|
|
1187
1194
|
<TagMenu label="Add tag" tags={tags} onPick={onAddTag} />
|
|
1188
1195
|
<TagMenu label="Remove tag" tags={tags} onPick={onRemoveTag} />
|
|
1196
|
+
<BatchAiActions
|
|
1197
|
+
documents={documents}
|
|
1198
|
+
onQueued={onQueued}
|
|
1199
|
+
buttonClassName="rounded-md px-2 py-1 text-sm text-[var(--txt)] transition-colors hover:bg-[var(--acc-l)]"
|
|
1200
|
+
/>
|
|
1189
1201
|
<button
|
|
1190
1202
|
type="button"
|
|
1191
1203
|
onClick={onDelete}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
X,
|
|
17
17
|
} from 'lucide-react'
|
|
18
18
|
import { toast } from 'sonner'
|
|
19
|
+
import { BatchAiActions } from '../../components/BatchAiActions.js'
|
|
19
20
|
import { useVirtualWindow, VirtualTablePadding } from '../../components/virtual-window.js'
|
|
20
21
|
import {
|
|
21
22
|
bulkDeletePosts,
|
|
@@ -345,6 +346,8 @@ export function PostsListView({ onNavigate }: PostsListViewProps) {
|
|
|
345
346
|
onPublish={() => runBulkUpdate('PUBLISHED')}
|
|
346
347
|
onDraft={() => runBulkUpdate('DRAFT')}
|
|
347
348
|
onDelete={() => setPendingBulk('delete')}
|
|
349
|
+
documents={selectedPosts.map((post) => ({ collection: post.postTypeId, id: post.id }))}
|
|
350
|
+
onQueued={() => setSelected(new Set())}
|
|
348
351
|
/>
|
|
349
352
|
)}
|
|
350
353
|
|
|
@@ -746,12 +749,16 @@ function BulkActionBar({
|
|
|
746
749
|
onPublish,
|
|
747
750
|
onDraft,
|
|
748
751
|
onDelete,
|
|
752
|
+
documents,
|
|
753
|
+
onQueued,
|
|
749
754
|
}: {
|
|
750
755
|
count: number
|
|
751
756
|
onClear: () => void
|
|
752
757
|
onPublish: () => void
|
|
753
758
|
onDraft: () => void
|
|
754
759
|
onDelete: () => void
|
|
760
|
+
documents: Array<{ collection: string; id: string }>
|
|
761
|
+
onQueued: () => void
|
|
755
762
|
}) {
|
|
756
763
|
return (
|
|
757
764
|
<div className="border-primary/30 bg-primary/5 flex items-center gap-2 rounded-lg border px-4 py-2">
|
|
@@ -771,6 +778,7 @@ function BulkActionBar({
|
|
|
771
778
|
>
|
|
772
779
|
Move to draft
|
|
773
780
|
</button>
|
|
781
|
+
<BatchAiActions documents={documents} onQueued={onQueued} />
|
|
774
782
|
<button
|
|
775
783
|
type="button"
|
|
776
784
|
onClick={onDelete}
|
|
@@ -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}
|