@byline/host-tanstack-start 4.19.0 → 5.0.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/dist/admin-shell/collections/edit.d.ts +1 -1
- package/dist/admin-shell/collections/edit.js +151 -31
- package/dist/admin-shell/collections/history.js +4 -2
- package/dist/admin-shell/collections/list.js +30 -6
- package/dist/admin-shell/collections/restore-version-modal.d.ts +3 -1
- package/dist/admin-shell/collections/restore-version-modal.js +9 -4
- package/dist/admin-shell/collections/tree-list.js +16 -6
- package/dist/admin-shell/collections/version-history.d.ts +2 -1
- package/dist/admin-shell/collections/version-history.js +11 -2
- package/dist/admin-shell/document-mutation-notice.d.ts +6 -0
- package/dist/admin-shell/document-mutation-notice.js +53 -0
- package/dist/admin-shell/document-mutation-state.d.ts +22 -0
- package/dist/admin-shell/document-mutation-state.js +71 -0
- package/dist/admin-shell/document-mutation-state.test.d.ts +1 -0
- package/dist/admin-shell/scheduled-publications/list.js +15 -5
- package/dist/admin-shell/singletons/history.js +4 -2
- package/dist/admin-shell/singletons/view.d.ts +1 -1
- package/dist/admin-shell/singletons/view.js +98 -19
- package/dist/integrations/byline-field-services.js +2 -9
- package/dist/routes/create-collection-edit-route.js +1 -1
- package/dist/routes/create-collection-list-route.d.ts +0 -7
- package/dist/routes/create-collection-list-route.js +7 -9
- package/dist/routes/create-singleton-route.d.ts +2 -0
- package/dist/routes/create-singleton-route.js +4 -3
- package/dist/server-fns/collections/copy-to-locale.d.ts +2 -7
- package/dist/server-fns/collections/copy-to-locale.js +4 -2
- package/dist/server-fns/collections/create.d.ts +1 -7
- package/dist/server-fns/collections/create.js +5 -3
- package/dist/server-fns/collections/delete-locale.d.ts +2 -7
- package/dist/server-fns/collections/delete-locale.js +4 -2
- package/dist/server-fns/collections/delete-outcome.d.ts +7 -3
- package/dist/server-fns/collections/delete-outcome.js +4 -0
- package/dist/server-fns/collections/delete.d.ts +2 -7
- package/dist/server-fns/collections/delete.js +5 -3
- package/dist/server-fns/collections/duplicate.d.ts +2 -7
- package/dist/server-fns/collections/duplicate.js +5 -3
- package/dist/server-fns/collections/get.d.ts +1 -7
- package/dist/server-fns/collections/get.js +5 -4
- package/dist/server-fns/collections/list.d.ts +0 -7
- package/dist/server-fns/collections/list.js +11 -3
- package/dist/server-fns/collections/reorder.d.ts +7 -8
- package/dist/server-fns/collections/reorder.js +20 -76
- package/dist/server-fns/collections/restore-version.d.ts +3 -7
- package/dist/server-fns/collections/restore-version.js +5 -2
- package/dist/server-fns/collections/save-outcome.d.ts +1 -0
- package/dist/server-fns/collections/save-outcome.js +1 -0
- package/dist/server-fns/collections/scheduled-publication.d.ts +49 -9
- package/dist/server-fns/collections/scheduled-publication.js +31 -21
- package/dist/server-fns/collections/status.d.ts +8 -7
- package/dist/server-fns/collections/status.js +13 -5
- package/dist/server-fns/collections/tree-mutation.d.ts +7 -6
- package/dist/server-fns/collections/tree-mutation.js +17 -5
- package/dist/server-fns/collections/tree.d.ts +14 -8
- package/dist/server-fns/collections/tree.js +21 -17
- package/dist/server-fns/collections/update.d.ts +12 -11
- package/dist/server-fns/collections/update.js +34 -20
- package/dist/server-fns/collections/upload.d.ts +1 -0
- package/dist/server-fns/collections/upload.js +3 -2
- package/dist/server-fns/document-mutation-errors.d.ts +5 -0
- package/dist/server-fns/document-mutation-errors.js +50 -0
- package/dist/server-fns/editable-revision.d.ts +9 -0
- package/dist/server-fns/editable-revision.js +11 -0
- package/dist/server-fns/singleton-document-read.d.ts +1 -1
- package/dist/server-fns/singleton-document-read.js +7 -3
- package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +6 -8
- package/dist/server-fns/singletons/cancel-scheduled-publish.js +4 -1
- package/dist/server-fns/singletons/change-status.d.ts +2 -7
- package/dist/server-fns/singletons/change-status.js +4 -1
- package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +5 -8
- package/dist/server-fns/singletons/confirm-scheduled-publish.js +4 -2
- package/dist/server-fns/singletons/copy-to-locale.d.ts +2 -7
- package/dist/server-fns/singletons/copy-to-locale.js +4 -2
- package/dist/server-fns/singletons/get.d.ts +4 -4
- package/dist/server-fns/singletons/restore-version.d.ts +2 -7
- package/dist/server-fns/singletons/restore-version.js +6 -3
- package/dist/server-fns/singletons/schedule-publish.d.ts +5 -8
- package/dist/server-fns/singletons/schedule-publish.js +4 -2
- package/dist/server-fns/singletons/unpublish.d.ts +2 -7
- package/dist/server-fns/singletons/unpublish.js +4 -1
- package/dist/server-fns/singletons/update.d.ts +4 -11
- package/dist/server-fns/singletons/update.js +5 -4
- package/dist/server-fns/structural-receipt.d.ts +4 -0
- package/dist/server-fns/structural-receipt.js +31 -0
- package/package.json +11 -11
- package/src/admin-shell/collections/edit.tsx +173 -54
- package/src/admin-shell/collections/history.tsx +11 -2
- package/src/admin-shell/collections/list.tsx +22 -6
- package/src/admin-shell/collections/restore-version-modal.tsx +12 -3
- package/src/admin-shell/collections/save-navigation.test.tsx +156 -8
- package/src/admin-shell/collections/tree-list.tsx +16 -6
- package/src/admin-shell/collections/version-history.tsx +13 -3
- package/src/admin-shell/document-mutation-notice.tsx +45 -0
- package/src/admin-shell/document-mutation-state.test.tsx +146 -0
- package/src/admin-shell/document-mutation-state.ts +95 -0
- package/src/admin-shell/scheduled-publications/list.tsx +16 -6
- package/src/admin-shell/singletons/history.test.tsx +3 -1
- package/src/admin-shell/singletons/history.tsx +10 -2
- package/src/admin-shell/singletons/view.test.tsx +59 -18
- package/src/admin-shell/singletons/view.tsx +110 -29
- package/src/integrations/byline-field-services.ts +2 -3
- package/src/routes/create-collection-edit-route.tsx +1 -0
- package/src/routes/create-collection-list-route.tsx +13 -2
- package/src/routes/create-singleton-route.test.node.ts +10 -2
- package/src/routes/create-singleton-route.tsx +6 -4
- package/src/server-fns/collections/copy-to-locale.ts +41 -36
- package/src/server-fns/collections/create.ts +54 -50
- package/src/server-fns/collections/delete-locale.ts +41 -32
- package/src/server-fns/collections/delete-outcome.test.node.ts +16 -0
- package/src/server-fns/collections/delete-outcome.ts +27 -16
- package/src/server-fns/collections/delete.ts +44 -38
- package/src/server-fns/collections/duplicate.ts +38 -32
- package/src/server-fns/collections/get.ts +8 -4
- package/src/server-fns/collections/list.ts +8 -3
- package/src/server-fns/collections/reorder.ts +32 -124
- package/src/server-fns/collections/restore-version.ts +47 -39
- package/src/server-fns/collections/revision-input.test.node.ts +108 -0
- package/src/server-fns/collections/save-outcome.test.node.ts +3 -0
- package/src/server-fns/collections/save-outcome.ts +2 -0
- package/src/server-fns/collections/scheduled-publication.ts +138 -111
- package/src/server-fns/collections/status.ts +86 -64
- package/src/server-fns/collections/tree-mutation.test.node.ts +87 -29
- package/src/server-fns/collections/tree-mutation.ts +28 -11
- package/src/server-fns/collections/tree.ts +150 -122
- package/src/server-fns/collections/update.ts +110 -86
- package/src/server-fns/collections/upload-singleton.test.node.ts +3 -7
- package/src/server-fns/collections/upload.ts +81 -77
- package/src/server-fns/document-mutation-errors.ts +67 -0
- package/src/server-fns/document-mutation-transport.test.node.ts +211 -0
- package/src/server-fns/editable-revision.test.node.ts +23 -0
- package/src/server-fns/editable-revision.ts +18 -0
- package/src/server-fns/singleton-document-read.ts +7 -3
- package/src/server-fns/singletons/cancel-scheduled-publish.ts +10 -3
- package/src/server-fns/singletons/change-status.ts +10 -3
- package/src/server-fns/singletons/confirm-scheduled-publish.ts +12 -6
- package/src/server-fns/singletons/copy-to-locale.ts +20 -15
- package/src/server-fns/singletons/get.ts +3 -3
- package/src/server-fns/singletons/restore-version.ts +17 -12
- package/src/server-fns/singletons/schedule-publish.ts +18 -7
- package/src/server-fns/singletons/singletons.test.node.ts +63 -25
- package/src/server-fns/singletons/unpublish.ts +10 -3
- package/src/server-fns/singletons/update.ts +21 -16
- package/src/server-fns/structural-receipt.ts +41 -0
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import { DocumentMutationNotice } from '../document-mutation-notice.js'
|
|
2
|
+
import { useDocumentMutationState } from '../document-mutation-state.js'
|
|
3
|
+
|
|
4
|
+
;('use client')
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
7
|
* This Source Code is subject to the terms of the Mozilla Public
|
|
@@ -60,6 +63,7 @@ export function ScheduledPublicationsView({ data }: { data: ScheduledPublication
|
|
|
60
63
|
state?: 'armed' | 'needs_reconfirm'
|
|
61
64
|
page?: number
|
|
62
65
|
}
|
|
66
|
+
const mutation = useDocumentMutationState(undefined)
|
|
63
67
|
const [cancelling, setCancelling] = useState<string | null>(null)
|
|
64
68
|
|
|
65
69
|
const applySearch = (patch: Record<string, unknown>) => {
|
|
@@ -69,11 +73,13 @@ export function ScheduledPublicationsView({ data }: { data: ScheduledPublication
|
|
|
69
73
|
})
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
const cancel = async (collection: string, documentId: string) => {
|
|
76
|
+
const cancel = async (collection: string, documentId: string, expectedRevision: number) => {
|
|
77
|
+
if (mutation.blocked || cancelling) return
|
|
73
78
|
setCancelling(documentId)
|
|
74
79
|
try {
|
|
80
|
+
mutation.assertWritable()
|
|
75
81
|
const result = await cancelCollectionDocumentScheduledPublish({
|
|
76
|
-
data: { collection, id: documentId },
|
|
82
|
+
data: { collection, id: documentId, expectedRevision },
|
|
77
83
|
})
|
|
78
84
|
const cancelled = result.status === 'cancelled'
|
|
79
85
|
toastManager.add({
|
|
@@ -95,9 +101,10 @@ export function ScheduledPublicationsView({ data }: { data: ScheduledPublication
|
|
|
95
101
|
search: location.search as Record<string, unknown>,
|
|
96
102
|
})
|
|
97
103
|
} catch (error) {
|
|
104
|
+
if (mutation.report(error)) return
|
|
98
105
|
toastManager.add({
|
|
99
106
|
title: t('scheduledPublication.toast.failedTitle'),
|
|
100
|
-
description: (
|
|
107
|
+
description: t('documentConcurrency.failed'),
|
|
101
108
|
data: { intent: 'danger', iconType: 'danger', icon: true, close: true },
|
|
102
109
|
})
|
|
103
110
|
} finally {
|
|
@@ -110,6 +117,7 @@ export function ScheduledPublicationsView({ data }: { data: ScheduledPublication
|
|
|
110
117
|
return (
|
|
111
118
|
<Section>
|
|
112
119
|
<Container>
|
|
120
|
+
<DocumentMutationNotice issue={mutation.issue} />
|
|
113
121
|
<div className={cx('byline-scheduled-list-head', styles.head)}>
|
|
114
122
|
<h1 className={styles.title}>{t('scheduledPublication.list.title')}</h1>
|
|
115
123
|
<p className={styles.intro}>{t('scheduledPublication.list.intro')}</p>
|
|
@@ -257,13 +265,15 @@ export function ScheduledPublicationsView({ data }: { data: ScheduledPublication
|
|
|
257
265
|
size="xs"
|
|
258
266
|
type="button"
|
|
259
267
|
intent="danger"
|
|
260
|
-
disabled={cancelling
|
|
268
|
+
disabled={mutation.blocked || cancelling !== null}
|
|
261
269
|
// The column header already says Actions and the row
|
|
262
270
|
// is a schedule, so the short label carries here —
|
|
263
271
|
// unlike the editor's menu, where "Cancel schedule"
|
|
264
272
|
// has to distinguish itself from cancelling an edit.
|
|
265
273
|
aria-label={t('scheduledPublication.actions.cancel')}
|
|
266
|
-
onClick={() =>
|
|
274
|
+
onClick={() =>
|
|
275
|
+
cancel(schedule.collectionPath, schedule.documentId, schedule.revision)
|
|
276
|
+
}
|
|
267
277
|
>
|
|
268
278
|
{t('common.actions.cancel')}
|
|
269
279
|
</Button>
|
|
@@ -194,6 +194,7 @@ const data: VersionHistoryData = {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
const currentDocument = {
|
|
197
|
+
revision: 7,
|
|
197
198
|
id: 'document-settings',
|
|
198
199
|
versionId: 'version-current',
|
|
199
200
|
status: 'published',
|
|
@@ -266,7 +267,7 @@ describe('singleton version history', () => {
|
|
|
266
267
|
await act(async () => confirm?.click())
|
|
267
268
|
|
|
268
269
|
expect(mocks.restoreSingleton).toHaveBeenCalledWith({
|
|
269
|
-
data: { singleton: 'site-settings', versionId: 'version-old' },
|
|
270
|
+
data: { singleton: 'site-settings', versionId: 'version-old', expectedRevision: 7 },
|
|
270
271
|
})
|
|
271
272
|
expect(mocks.invalidate).toHaveBeenCalledTimes(1)
|
|
272
273
|
expect(mocks.navigate).toHaveBeenLastCalledWith({
|
|
@@ -395,6 +396,7 @@ describe('singleton version history', () => {
|
|
|
395
396
|
|
|
396
397
|
expect(mocks.restoreCollection).toHaveBeenCalledWith({
|
|
397
398
|
data: {
|
|
399
|
+
expectedRevision: 7,
|
|
398
400
|
collection: 'articles',
|
|
399
401
|
id: 'document-article',
|
|
400
402
|
versionId: 'version-old',
|
|
@@ -89,6 +89,7 @@ export function SingletonHistoryView({
|
|
|
89
89
|
currentVersionId,
|
|
90
90
|
actorLabel,
|
|
91
91
|
restoreStatusLabel,
|
|
92
|
+
mutationsBlocked,
|
|
92
93
|
openRestore,
|
|
93
94
|
}) => (
|
|
94
95
|
<>
|
|
@@ -106,6 +107,7 @@ export function SingletonHistoryView({
|
|
|
106
107
|
variant="outlined"
|
|
107
108
|
size="xs"
|
|
108
109
|
intent="noeffect"
|
|
110
|
+
disabled={mutationsBlocked}
|
|
109
111
|
onClick={openRestore}
|
|
110
112
|
className={cx('byline-coll-history-restore-button', historyStyles.restoreButton)}
|
|
111
113
|
title={t('collections.history.restoreButtonTitle', {
|
|
@@ -166,8 +168,14 @@ export function SingletonHistoryView({
|
|
|
166
168
|
return historical as unknown as Record<string, unknown>
|
|
167
169
|
}}
|
|
168
170
|
onPageSizeChange={handlePageSizeChange}
|
|
169
|
-
restoreVersion={(versionId) =>
|
|
170
|
-
restoreSingletonVersion({
|
|
171
|
+
restoreVersion={(versionId, expectedRevision) =>
|
|
172
|
+
restoreSingletonVersion({
|
|
173
|
+
data: {
|
|
174
|
+
expectedRevision,
|
|
175
|
+
singleton,
|
|
176
|
+
versionId,
|
|
177
|
+
},
|
|
178
|
+
})
|
|
171
179
|
}
|
|
172
180
|
onRestoreComplete={async () => {
|
|
173
181
|
await router.invalidate()
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
import { act } from 'react'
|
|
10
10
|
|
|
11
11
|
import { BylineFieldServicesProvider } from '@byline/admin/react'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
defineAdminConfig,
|
|
14
|
+
defineSingleton,
|
|
15
|
+
ERR_DOCUMENT_STALE,
|
|
16
|
+
type SingletonDefinition,
|
|
17
|
+
} from '@byline/core'
|
|
13
18
|
import { adminTranslations } from '@byline/i18n/admin'
|
|
14
19
|
import { I18nProvider } from '@byline/i18n/react'
|
|
15
20
|
import { createRoot, type Root } from 'react-dom/client'
|
|
@@ -149,6 +154,7 @@ const locales = [
|
|
|
149
154
|
const loadedDocument = {
|
|
150
155
|
id: 'doc-settings',
|
|
151
156
|
versionId: 'version-1',
|
|
157
|
+
revision: 7,
|
|
152
158
|
path: '__singleton__/site-settings',
|
|
153
159
|
status: 'draft',
|
|
154
160
|
fields: { title: 'Loaded title', attachment: null },
|
|
@@ -233,13 +239,29 @@ function latestToastDescription(): string {
|
|
|
233
239
|
beforeEach(() => {
|
|
234
240
|
vi.clearAllMocks()
|
|
235
241
|
mocks.guardStates.length = 0
|
|
236
|
-
mocks.update.mockResolvedValue({
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
mocks.
|
|
242
|
-
|
|
242
|
+
mocks.update.mockResolvedValue({
|
|
243
|
+
documentId: 'doc-settings',
|
|
244
|
+
revision: 8,
|
|
245
|
+
versionId: 'version-next',
|
|
246
|
+
})
|
|
247
|
+
mocks.changeStatus.mockResolvedValue({
|
|
248
|
+
documentId: 'doc-settings',
|
|
249
|
+
revision: 8,
|
|
250
|
+
newStatus: 'published',
|
|
251
|
+
})
|
|
252
|
+
mocks.unpublish.mockResolvedValue({ documentId: 'doc-settings', revision: 8, archivedCount: 1 })
|
|
253
|
+
mocks.schedule.mockResolvedValue({ documentId: 'doc-settings', revision: 8, state: 'armed' })
|
|
254
|
+
mocks.confirmSchedule.mockResolvedValue({
|
|
255
|
+
documentId: 'doc-settings',
|
|
256
|
+
revision: 8,
|
|
257
|
+
state: 'armed',
|
|
258
|
+
})
|
|
259
|
+
mocks.cancelSchedule.mockResolvedValue({ schedule: null, revision: 8 })
|
|
260
|
+
mocks.copyToLocale.mockResolvedValue({
|
|
261
|
+
documentId: 'doc-settings',
|
|
262
|
+
revision: 8,
|
|
263
|
+
versionId: 'version-next',
|
|
264
|
+
})
|
|
243
265
|
|
|
244
266
|
defineAdminConfig({
|
|
245
267
|
routes: { admin: '/internal/cms' },
|
|
@@ -316,7 +338,7 @@ describe('SingletonView', () => {
|
|
|
316
338
|
expect(container.querySelector('.byline-preview-link')).toBeNull()
|
|
317
339
|
})
|
|
318
340
|
|
|
319
|
-
it('saves full data with the active locale and the
|
|
341
|
+
it('saves full data with the active locale and the observed revision or explicit empty slot', async () => {
|
|
320
342
|
render(null, 'en')
|
|
321
343
|
typeIntoTitle('First save')
|
|
322
344
|
await submit()
|
|
@@ -326,7 +348,7 @@ describe('SingletonView', () => {
|
|
|
326
348
|
singleton: 'site-settings',
|
|
327
349
|
data: { title: 'First save', attachment: undefined },
|
|
328
350
|
locale: 'en',
|
|
329
|
-
|
|
351
|
+
expectedState: 'empty',
|
|
330
352
|
},
|
|
331
353
|
})
|
|
332
354
|
expect(mocks.invalidate).toHaveBeenCalledTimes(1)
|
|
@@ -342,19 +364,28 @@ describe('SingletonView', () => {
|
|
|
342
364
|
singleton: 'site-settings',
|
|
343
365
|
data: { title: 'Second save', attachment: null },
|
|
344
366
|
locale: 'fr',
|
|
345
|
-
|
|
367
|
+
expectedRevision: 7,
|
|
346
368
|
},
|
|
347
369
|
})
|
|
348
370
|
expect(mocks.invalidate).toHaveBeenCalledTimes(2)
|
|
349
371
|
})
|
|
350
372
|
|
|
351
373
|
it('distinguishes stale saves from a singleton missing on the server', async () => {
|
|
352
|
-
mocks.update.mockRejectedValueOnce(
|
|
374
|
+
mocks.update.mockRejectedValueOnce(
|
|
375
|
+
ERR_DOCUMENT_STALE({
|
|
376
|
+
message: 'Test failure',
|
|
377
|
+
details: {
|
|
378
|
+
reason: 'revision_mismatch',
|
|
379
|
+
documentId: 'doc-settings',
|
|
380
|
+
expectedRevision: 7,
|
|
381
|
+
currentRevision: 8,
|
|
382
|
+
},
|
|
383
|
+
})
|
|
384
|
+
)
|
|
353
385
|
render(loadedDocument)
|
|
354
386
|
typeIntoTitle('Stale edit')
|
|
355
387
|
await submit()
|
|
356
|
-
|
|
357
|
-
expect(conflict).toContain('Someone else saved this singleton first')
|
|
388
|
+
expect(container.textContent).toContain('This document has changed since you opened it.')
|
|
358
389
|
|
|
359
390
|
act(() => root.unmount())
|
|
360
391
|
root = createRoot(container)
|
|
@@ -362,13 +393,22 @@ describe('SingletonView', () => {
|
|
|
362
393
|
render(loadedDocument)
|
|
363
394
|
typeIntoTitle('Missing edit')
|
|
364
395
|
await submit()
|
|
365
|
-
|
|
366
|
-
expect(
|
|
367
|
-
expect(notConfigured).not.toBe(conflict)
|
|
396
|
+
expect(container.textContent).toContain('This document is no longer available.')
|
|
397
|
+
expect(container.textContent).not.toContain('This document has changed since you opened it.')
|
|
368
398
|
})
|
|
369
399
|
|
|
370
400
|
it('keeps a failed save dirty with the navigation guard active', async () => {
|
|
371
|
-
mocks.update.mockRejectedValueOnce(
|
|
401
|
+
mocks.update.mockRejectedValueOnce(
|
|
402
|
+
ERR_DOCUMENT_STALE({
|
|
403
|
+
message: 'Test failure',
|
|
404
|
+
details: {
|
|
405
|
+
reason: 'revision_mismatch',
|
|
406
|
+
documentId: 'doc-settings',
|
|
407
|
+
expectedRevision: 7,
|
|
408
|
+
currentRevision: 8,
|
|
409
|
+
},
|
|
410
|
+
})
|
|
411
|
+
)
|
|
372
412
|
render(loadedDocument)
|
|
373
413
|
typeIntoTitle('Unsaved change')
|
|
374
414
|
await submit()
|
|
@@ -383,6 +423,7 @@ describe('SingletonView', () => {
|
|
|
383
423
|
status: 'committed-hook-failed',
|
|
384
424
|
documentId: 'doc-settings',
|
|
385
425
|
documentVersionId: 'version-next',
|
|
426
|
+
revision: 8,
|
|
386
427
|
sideEffectFailure: { phase: 'afterSave', code: 'ERR_UNHANDLED' },
|
|
387
428
|
})
|
|
388
429
|
render(loadedDocument)
|
|
@@ -13,7 +13,6 @@ import { useRouter } from '@tanstack/react-router'
|
|
|
13
13
|
import { FormRenderer } from '@byline/admin/react'
|
|
14
14
|
import type { SingletonAdminConfig, SingletonDefinition } from '@byline/core'
|
|
15
15
|
import {
|
|
16
|
-
ErrorCodes,
|
|
17
16
|
getDefaultStatus,
|
|
18
17
|
getWorkflow,
|
|
19
18
|
getWorkflowStatuses,
|
|
@@ -38,6 +37,7 @@ import {
|
|
|
38
37
|
} from '../../server-fns/singletons/index.js'
|
|
39
38
|
import { useNavigate } from '../chrome/loose-router.js'
|
|
40
39
|
import { useTanStackNavigationGuard } from '../collections/tanstack-navigation-guard.js'
|
|
40
|
+
import { useDocumentMutationState } from '../document-mutation-state.js'
|
|
41
41
|
import { SingletonViewMenu } from './view-menu.js'
|
|
42
42
|
import type { ContentLocaleOption } from '../collections/view-menu.js'
|
|
43
43
|
|
|
@@ -47,16 +47,10 @@ type SingletonDocument = Record<string, any> & {
|
|
|
47
47
|
fields: Record<string, any>
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function errorCode(error: unknown): string | null {
|
|
51
|
-
return typeof (error as { code?: unknown })?.code === 'string'
|
|
52
|
-
? (error as { code: string }).code
|
|
53
|
-
: null
|
|
54
|
-
}
|
|
55
|
-
|
|
56
50
|
export function SingletonView({
|
|
57
51
|
singletonDefinition,
|
|
58
52
|
adminConfig,
|
|
59
|
-
document,
|
|
53
|
+
document: loadedDocument,
|
|
60
54
|
initialData,
|
|
61
55
|
locale,
|
|
62
56
|
contentLocales,
|
|
@@ -71,6 +65,8 @@ export function SingletonView({
|
|
|
71
65
|
contentLocales: ReadonlyArray<ContentLocaleOption>
|
|
72
66
|
defaultContentLocale: string
|
|
73
67
|
}) {
|
|
68
|
+
const mutation = useDocumentMutationState(loadedDocument)
|
|
69
|
+
const document = mutation.document ?? null
|
|
74
70
|
const router = useRouter()
|
|
75
71
|
const navigate = useNavigate()
|
|
76
72
|
const toastManager = useToastManager()
|
|
@@ -132,14 +128,18 @@ export function SingletonView({
|
|
|
132
128
|
|
|
133
129
|
const handleSubmit = async ({ data }: { data: Record<string, any> }) => {
|
|
134
130
|
try {
|
|
131
|
+
mutation.assertWritable()
|
|
135
132
|
const result = await updateSingleton({
|
|
136
133
|
data: {
|
|
137
134
|
singleton: path,
|
|
138
135
|
data,
|
|
139
136
|
locale: locale ?? defaultContentLocale,
|
|
140
|
-
|
|
137
|
+
...(document == null
|
|
138
|
+
? { expectedState: 'empty' as const }
|
|
139
|
+
: { expectedRevision: mutation.expectedRevision() }),
|
|
141
140
|
},
|
|
142
141
|
})
|
|
142
|
+
mutation.adopt(result)
|
|
143
143
|
if (document != null) notifyScheduleSuspended()
|
|
144
144
|
if (hasCommittedDocumentHookFailure(result)) {
|
|
145
145
|
notifyCommittedHookFailure()
|
|
@@ -152,13 +152,16 @@ export function SingletonView({
|
|
|
152
152
|
}
|
|
153
153
|
await reload()
|
|
154
154
|
} catch (err) {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
const handled = mutation.report(err)
|
|
156
|
+
if (handled === 'blocked') throw err
|
|
157
|
+
if (handled === 'committed') {
|
|
158
|
+
notifyCommittedHookFailure()
|
|
159
|
+
await reload()
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
const description = t('singletons.edit.updateFailedDescription', {
|
|
163
|
+
label: label.toLowerCase(),
|
|
164
|
+
})
|
|
162
165
|
toast(t('singletons.edit.updateTitle', { label }), description, 'danger')
|
|
163
166
|
|
|
164
167
|
// FormRenderer commits its clean baseline only when this promise
|
|
@@ -170,7 +173,11 @@ export function SingletonView({
|
|
|
170
173
|
|
|
171
174
|
const handleStatusChange = async (status: string) => {
|
|
172
175
|
try {
|
|
173
|
-
|
|
176
|
+
mutation.assertWritable()
|
|
177
|
+
const result = await changeSingletonStatus({
|
|
178
|
+
data: { expectedRevision: mutation.expectedRevision(), singleton: path, status },
|
|
179
|
+
})
|
|
180
|
+
mutation.adopt(result)
|
|
174
181
|
toast(
|
|
175
182
|
t('collections.edit.statusUpdateTitle', { label }),
|
|
176
183
|
t('collections.edit.statusChangedDescription', { status }),
|
|
@@ -178,9 +185,18 @@ export function SingletonView({
|
|
|
178
185
|
)
|
|
179
186
|
await reload()
|
|
180
187
|
} catch (err) {
|
|
188
|
+
const handled = mutation.report(err)
|
|
189
|
+
if (handled === 'blocked') throw err
|
|
190
|
+
if (handled === 'committed') {
|
|
191
|
+
notifyCommittedHookFailure()
|
|
192
|
+
await reload()
|
|
193
|
+
return
|
|
194
|
+
}
|
|
181
195
|
toast(
|
|
182
196
|
t('collections.edit.statusUpdateTitle', { label }),
|
|
183
|
-
t('collections.edit.statusChangeFailedDescription', {
|
|
197
|
+
t('collections.edit.statusChangeFailedDescription', {
|
|
198
|
+
message: t('documentConcurrency.failed'),
|
|
199
|
+
}),
|
|
184
200
|
'danger'
|
|
185
201
|
)
|
|
186
202
|
throw err
|
|
@@ -189,7 +205,11 @@ export function SingletonView({
|
|
|
189
205
|
|
|
190
206
|
const handleUnpublish = async () => {
|
|
191
207
|
try {
|
|
192
|
-
|
|
208
|
+
mutation.assertWritable()
|
|
209
|
+
const result = await unpublishSingleton({
|
|
210
|
+
data: { expectedRevision: mutation.expectedRevision(), singleton: path },
|
|
211
|
+
})
|
|
212
|
+
mutation.adopt(result)
|
|
193
213
|
toast(
|
|
194
214
|
t('collections.edit.unpublishTitle', { label }),
|
|
195
215
|
t('collections.edit.unpublishedDescription'),
|
|
@@ -197,9 +217,18 @@ export function SingletonView({
|
|
|
197
217
|
)
|
|
198
218
|
await reload()
|
|
199
219
|
} catch (err) {
|
|
220
|
+
const handled = mutation.report(err)
|
|
221
|
+
if (handled === 'blocked') throw err
|
|
222
|
+
if (handled === 'committed') {
|
|
223
|
+
notifyCommittedHookFailure()
|
|
224
|
+
await reload()
|
|
225
|
+
return
|
|
226
|
+
}
|
|
200
227
|
toast(
|
|
201
228
|
t('collections.edit.unpublishTitle', { label }),
|
|
202
|
-
t('collections.edit.unpublishFailedDescription', {
|
|
229
|
+
t('collections.edit.unpublishFailedDescription', {
|
|
230
|
+
message: t('documentConcurrency.failed'),
|
|
231
|
+
}),
|
|
203
232
|
'danger'
|
|
204
233
|
)
|
|
205
234
|
throw err
|
|
@@ -214,14 +243,17 @@ export function SingletonView({
|
|
|
214
243
|
overwrite: boolean
|
|
215
244
|
}) => {
|
|
216
245
|
try {
|
|
246
|
+
mutation.assertWritable()
|
|
217
247
|
const result = await copySingletonToLocale({
|
|
218
248
|
data: {
|
|
249
|
+
expectedRevision: mutation.expectedRevision(),
|
|
219
250
|
singleton: path,
|
|
220
251
|
sourceLocale: locale ?? defaultContentLocale,
|
|
221
252
|
targetLocale,
|
|
222
253
|
overwrite,
|
|
223
254
|
},
|
|
224
255
|
})
|
|
256
|
+
mutation.adopt(result)
|
|
225
257
|
const sourceLocale = locale ?? defaultContentLocale
|
|
226
258
|
const sourceLabel =
|
|
227
259
|
contentLocales.find((entry) => entry.code === sourceLocale)?.label ?? sourceLocale
|
|
@@ -246,9 +278,16 @@ export function SingletonView({
|
|
|
246
278
|
search: { locale: targetLocale },
|
|
247
279
|
})
|
|
248
280
|
} catch (err) {
|
|
281
|
+
const handled = mutation.report(err)
|
|
282
|
+
if (handled === 'blocked') throw err
|
|
283
|
+
if (handled === 'committed') {
|
|
284
|
+
notifyCommittedHookFailure()
|
|
285
|
+
await reload()
|
|
286
|
+
return
|
|
287
|
+
}
|
|
249
288
|
toast(
|
|
250
289
|
t('collections.edit.copyToLocaleTitle', { label }),
|
|
251
|
-
t('collections.edit.copyFailedDescription', { message: (
|
|
290
|
+
t('collections.edit.copyFailedDescription', { message: t('documentConcurrency.failed') }),
|
|
252
291
|
'danger'
|
|
253
292
|
)
|
|
254
293
|
throw err
|
|
@@ -257,9 +296,16 @@ export function SingletonView({
|
|
|
257
296
|
|
|
258
297
|
const handleSchedulePublication = async ({ publishAt }: { publishAt: string }) => {
|
|
259
298
|
try {
|
|
260
|
-
|
|
261
|
-
|
|
299
|
+
mutation.assertWritable()
|
|
300
|
+
const result = await scheduleSingletonPublish({
|
|
301
|
+
data: {
|
|
302
|
+
expectedRevision: mutation.expectedRevision(),
|
|
303
|
+
singleton: path,
|
|
304
|
+
publishAt,
|
|
305
|
+
expectedVersionId: String(document?.versionId),
|
|
306
|
+
},
|
|
262
307
|
})
|
|
308
|
+
mutation.adopt(result)
|
|
263
309
|
toast(
|
|
264
310
|
t('scheduledPublication.toast.scheduledTitle'),
|
|
265
311
|
t('scheduledPublication.toast.scheduledDescription'),
|
|
@@ -267,16 +313,29 @@ export function SingletonView({
|
|
|
267
313
|
)
|
|
268
314
|
await reload()
|
|
269
315
|
} catch (err) {
|
|
270
|
-
|
|
316
|
+
const handled = mutation.report(err)
|
|
317
|
+
if (handled === 'blocked') throw err
|
|
318
|
+
if (handled === 'committed') {
|
|
319
|
+
notifyCommittedHookFailure()
|
|
320
|
+
await reload()
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
toast(t('scheduledPublication.toast.failedTitle'), t('documentConcurrency.failed'), 'danger')
|
|
271
324
|
throw err
|
|
272
325
|
}
|
|
273
326
|
}
|
|
274
327
|
|
|
275
328
|
const handleConfirmScheduledPublication = async () => {
|
|
276
329
|
try {
|
|
277
|
-
|
|
278
|
-
|
|
330
|
+
mutation.assertWritable()
|
|
331
|
+
const result = await confirmSingletonScheduledPublish({
|
|
332
|
+
data: {
|
|
333
|
+
expectedRevision: mutation.expectedRevision(),
|
|
334
|
+
singleton: path,
|
|
335
|
+
expectedVersionId: String(document?.versionId),
|
|
336
|
+
},
|
|
279
337
|
})
|
|
338
|
+
mutation.adopt(result)
|
|
280
339
|
toast(
|
|
281
340
|
t('scheduledPublication.toast.confirmedTitle'),
|
|
282
341
|
t('scheduledPublication.toast.confirmedDescription'),
|
|
@@ -284,14 +343,25 @@ export function SingletonView({
|
|
|
284
343
|
)
|
|
285
344
|
await reload()
|
|
286
345
|
} catch (err) {
|
|
287
|
-
|
|
346
|
+
const handled = mutation.report(err)
|
|
347
|
+
if (handled === 'blocked') throw err
|
|
348
|
+
if (handled === 'committed') {
|
|
349
|
+
notifyCommittedHookFailure()
|
|
350
|
+
await reload()
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
toast(t('scheduledPublication.toast.failedTitle'), t('documentConcurrency.failed'), 'danger')
|
|
288
354
|
throw err
|
|
289
355
|
}
|
|
290
356
|
}
|
|
291
357
|
|
|
292
358
|
const handleCancelScheduledPublication = async () => {
|
|
293
359
|
try {
|
|
294
|
-
|
|
360
|
+
mutation.assertWritable()
|
|
361
|
+
const result = await cancelSingletonScheduledPublish({
|
|
362
|
+
data: { expectedRevision: mutation.expectedRevision(), singleton: path },
|
|
363
|
+
})
|
|
364
|
+
mutation.adopt({ ...result, documentId: String(document?.id) })
|
|
295
365
|
toast(
|
|
296
366
|
t('scheduledPublication.toast.cancelledTitle'),
|
|
297
367
|
t('scheduledPublication.toast.cancelledDescription'),
|
|
@@ -299,7 +369,14 @@ export function SingletonView({
|
|
|
299
369
|
)
|
|
300
370
|
await reload()
|
|
301
371
|
} catch (err) {
|
|
302
|
-
|
|
372
|
+
const handled = mutation.report(err)
|
|
373
|
+
if (handled === 'blocked') throw err
|
|
374
|
+
if (handled === 'committed') {
|
|
375
|
+
notifyCommittedHookFailure()
|
|
376
|
+
await reload()
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
toast(t('scheduledPublication.toast.failedTitle'), t('documentConcurrency.failed'), 'danger')
|
|
303
380
|
throw err
|
|
304
381
|
}
|
|
305
382
|
}
|
|
@@ -308,6 +385,10 @@ export function SingletonView({
|
|
|
308
385
|
<Section>
|
|
309
386
|
<Container>
|
|
310
387
|
<FormRenderer
|
|
388
|
+
mutationIssue={mutation.issue}
|
|
389
|
+
mutationsBlocked={mutation.blocked}
|
|
390
|
+
observedRevision={mutation.revision}
|
|
391
|
+
onMutationError={mutation.report}
|
|
311
392
|
mode={document == null ? 'create' : 'edit'}
|
|
312
393
|
fields={fields}
|
|
313
394
|
onSubmit={handleSubmit}
|
|
@@ -44,12 +44,11 @@ const uploadField: UploadFieldFn = (collection, formData, createDocument) =>
|
|
|
44
44
|
serverUploadField(collection, formData, createDocument)
|
|
45
45
|
|
|
46
46
|
const placeTreeNode: PlaceTreeNodeFn = async (input) => {
|
|
47
|
-
|
|
48
|
-
return { orderKey }
|
|
47
|
+
return serverPlaceTreeNode({ data: input })
|
|
49
48
|
}
|
|
50
49
|
|
|
51
50
|
const removeFromTree: RemoveFromTreeFn = async (input) => {
|
|
52
|
-
|
|
51
|
+
return serverRemoveFromTree({ data: input })
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
const getTreeAncestors: GetTreeAncestorsFn = (input) =>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseDocumentRevision } from '@byline/core'
|
|
1
2
|
/**
|
|
2
3
|
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
4
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
@@ -177,8 +178,13 @@ export function createCollectionListRoute(path: string) {
|
|
|
177
178
|
beforeDocumentId,
|
|
178
179
|
afterDocumentId,
|
|
179
180
|
}) => {
|
|
180
|
-
await placeTreeNode({
|
|
181
|
+
return await placeTreeNode({
|
|
181
182
|
data: {
|
|
183
|
+
expectedRevision: parseDocumentRevision(
|
|
184
|
+
(data as { rows: CollectionTreeRow[] }).rows.find(
|
|
185
|
+
(row) => row.id === documentId
|
|
186
|
+
)?.revision
|
|
187
|
+
),
|
|
182
188
|
collection,
|
|
183
189
|
documentId,
|
|
184
190
|
parentDocumentId,
|
|
@@ -199,8 +205,13 @@ export function createCollectionListRoute(path: string) {
|
|
|
199
205
|
orderable={collectionDef.orderable === true}
|
|
200
206
|
listActions={adminConfig?.listActions}
|
|
201
207
|
onReorder={async ({ documentId, beforeDocumentId, afterDocumentId }) => {
|
|
202
|
-
await reorderCollectionDocument({
|
|
208
|
+
return await reorderCollectionDocument({
|
|
203
209
|
data: {
|
|
210
|
+
expectedRevision: parseDocumentRevision(
|
|
211
|
+
data.docs.find(
|
|
212
|
+
(row: { id: string; revision?: number }) => row.id === documentId
|
|
213
|
+
)?.revision
|
|
214
|
+
),
|
|
204
215
|
collection,
|
|
205
216
|
documentId,
|
|
206
217
|
beforeDocumentId,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { testAdapter } from '../../../core/src/storage/db-adapter.test-helper.js'
|
|
1
2
|
/**
|
|
2
3
|
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
4
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
@@ -61,7 +62,7 @@ vi.mock('@byline/core', () => ({
|
|
|
61
62
|
getServerConfig: () => ({
|
|
62
63
|
i18n: { content: { defaultLocale: 'en' } },
|
|
63
64
|
scheduledPublication: { enabled: mocks.scheduledPublicationEnabled },
|
|
64
|
-
db: { queries: { documents: { getPublishedVersion: mocks.getPublishedVersion } } },
|
|
65
|
+
db: testAdapter({ queries: { documents: { getPublishedVersion: mocks.getPublishedVersion } } }),
|
|
65
66
|
}),
|
|
66
67
|
getSingletonAdminConfig: () => null,
|
|
67
68
|
}))
|
|
@@ -112,6 +113,7 @@ describe('singleton editor route loader', () => {
|
|
|
112
113
|
|
|
113
114
|
await expect(load('fr')).resolves.toEqual({
|
|
114
115
|
document: null,
|
|
116
|
+
expectedEmpty: true,
|
|
115
117
|
initialData: { title: 'Default title' },
|
|
116
118
|
})
|
|
117
119
|
expect(mocks.read).toHaveBeenCalledWith({
|
|
@@ -130,6 +132,12 @@ describe('singleton editor route loader', () => {
|
|
|
130
132
|
versionId: 'version-current',
|
|
131
133
|
status: 'draft',
|
|
132
134
|
fields: { title: 'Loaded' },
|
|
135
|
+
revision: 7,
|
|
136
|
+
scheduledPublication: {
|
|
137
|
+
state: 'armed',
|
|
138
|
+
publishAt: '2026-09-01T00:00:00.000Z',
|
|
139
|
+
targetVersionId: 'version-current',
|
|
140
|
+
},
|
|
133
141
|
})
|
|
134
142
|
mocks.getPublishedVersion.mockResolvedValue({
|
|
135
143
|
document_id: 'document-settings',
|
|
@@ -167,6 +175,6 @@ describe('singleton editor route loader', () => {
|
|
|
167
175
|
collection_id: 'collection-settings',
|
|
168
176
|
document_id: 'document-settings',
|
|
169
177
|
})
|
|
170
|
-
expect(mocks.getScheduledPublish).
|
|
178
|
+
expect(mocks.getScheduledPublish).not.toHaveBeenCalled()
|
|
171
179
|
})
|
|
172
180
|
})
|