@byline/host-tanstack-start 4.18.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/create.js +22 -5
- package/dist/admin-shell/collections/edit.d.ts +1 -1
- package/dist/admin-shell/collections/edit.js +222 -34
- 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 +23 -6
- package/dist/admin-shell/collections/save-navigation.test.d.ts +8 -0
- 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 +108 -23
- 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 +4 -8
- package/dist/server-fns/collections/copy-to-locale.js +17 -8
- package/dist/server-fns/collections/create.d.ts +2 -8
- package/dist/server-fns/collections/create.js +20 -10
- package/dist/server-fns/collections/delete-locale.d.ts +4 -8
- package/dist/server-fns/collections/delete-locale.js +15 -6
- 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 +4 -8
- package/dist/server-fns/collections/duplicate.js +14 -5
- package/dist/server-fns/collections/get.d.ts +1 -7
- package/dist/server-fns/collections/get.js +5 -4
- package/dist/server-fns/collections/index.d.ts +1 -0
- package/dist/server-fns/collections/index.js +1 -0
- 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 +4 -8
- package/dist/server-fns/collections/restore-version.js +22 -12
- package/dist/server-fns/collections/save-outcome.d.ts +24 -0
- package/dist/server-fns/collections/save-outcome.js +19 -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 +14 -11
- package/dist/server-fns/collections/update.js +48 -24
- 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 +16 -5
- 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 +13 -1
- 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 +16 -8
- 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/create.tsx +22 -3
- package/src/admin-shell/collections/edit.tsx +233 -61
- 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 +23 -5
- package/src/admin-shell/collections/save-navigation.test.tsx +532 -0
- 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 +46 -1
- package/src/admin-shell/singletons/history.tsx +10 -2
- package/src/admin-shell/singletons/view.test.tsx +86 -18
- package/src/admin-shell/singletons/view.tsx +145 -45
- 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 +48 -29
- package/src/server-fns/collections/create.ts +56 -44
- package/src/server-fns/collections/delete-locale.ts +48 -25
- 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 +45 -25
- package/src/server-fns/collections/get.ts +8 -4
- package/src/server-fns/collections/index.ts +1 -0
- 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 +51 -36
- package/src/server-fns/collections/revision-input.test.node.ts +108 -0
- package/src/server-fns/collections/save-outcome.test.node.ts +80 -0
- package/src/server-fns/collections/save-outcome.ts +57 -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 -71
- 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 -8
- package/src/server-fns/singletons/get.ts +3 -3
- package/src/server-fns/singletons/restore-version.ts +17 -3
- package/src/server-fns/singletons/schedule-publish.ts +18 -7
- package/src/server-fns/singletons/singletons.test.node.ts +96 -24
- package/src/server-fns/singletons/unpublish.ts +10 -3
- package/src/server-fns/singletons/update.ts +21 -9
- package/src/server-fns/structural-receipt.ts +41 -0
|
@@ -29,28 +29,29 @@ import {
|
|
|
29
29
|
deleteDocument,
|
|
30
30
|
deleteDocumentLocale,
|
|
31
31
|
duplicateCollectionDocument,
|
|
32
|
+
hasCommittedDocumentHookFailure,
|
|
32
33
|
hasDeleteSideEffectFailures,
|
|
33
34
|
scheduleCollectionDocumentPublish,
|
|
34
35
|
unpublishDocument,
|
|
35
|
-
updateCollectionDocumentSystemFields,
|
|
36
36
|
updateCollectionDocumentWithPatches,
|
|
37
37
|
updateDocumentStatus,
|
|
38
38
|
} from '../../server-fns/collections/index.js'
|
|
39
39
|
import { useNavigate } from '../chrome/loose-router.js'
|
|
40
|
+
import { useDocumentMutationState } from '../document-mutation-state.js'
|
|
40
41
|
import { useTanStackNavigationGuard } from './tanstack-navigation-guard.js'
|
|
41
42
|
import { ViewMenu } from './view-menu.js'
|
|
42
43
|
import type { SerializedDocumentPublishSchedule } from '../../server-fns/collections/index.js'
|
|
43
44
|
import type { ContentLocaleOption } from './view-menu.js'
|
|
44
45
|
|
|
45
46
|
type EditState = {
|
|
46
|
-
status: 'success' | 'failed' | 'busy' | 'idle'
|
|
47
|
+
status: 'success' | 'warning' | 'failed' | 'busy' | 'idle'
|
|
47
48
|
message: string
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export const EditView = ({
|
|
51
52
|
collectionDefinition,
|
|
52
53
|
adminConfig,
|
|
53
|
-
initialData,
|
|
54
|
+
initialData: loadedData,
|
|
54
55
|
locale,
|
|
55
56
|
contentLocales,
|
|
56
57
|
defaultContentLocale,
|
|
@@ -65,6 +66,8 @@ export const EditView = ({
|
|
|
65
66
|
/** URL-encoded list search state to return to on close — see list-return-state.ts. */
|
|
66
67
|
returnSearch?: Record<string, unknown>
|
|
67
68
|
}) => {
|
|
69
|
+
const mutation = useDocumentMutationState(loadedData)
|
|
70
|
+
const initialData = mutation.document ?? loadedData
|
|
68
71
|
const toastManager = useToastManager()
|
|
69
72
|
const { t } = useTranslation('byline-admin')
|
|
70
73
|
const [_editState, setEditState] = useState<EditState>({
|
|
@@ -110,6 +113,16 @@ export const EditView = ({
|
|
|
110
113
|
})
|
|
111
114
|
}
|
|
112
115
|
|
|
116
|
+
const notifyCommittedHookFailure = () => {
|
|
117
|
+
const description = t('collections.save.hookFailedDescription')
|
|
118
|
+
toastManager.add({
|
|
119
|
+
title: t('collections.save.hookFailedToast'),
|
|
120
|
+
description,
|
|
121
|
+
data: { intent: 'warning', iconType: 'warning', icon: true, close: true },
|
|
122
|
+
})
|
|
123
|
+
setEditState({ status: 'warning', message: description })
|
|
124
|
+
}
|
|
125
|
+
|
|
113
126
|
const handleLocaleChange = (newLocale: string) => {
|
|
114
127
|
navigate({
|
|
115
128
|
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
@@ -120,9 +133,16 @@ export const EditView = ({
|
|
|
120
133
|
|
|
121
134
|
const handleStatusChange = async (status: string) => {
|
|
122
135
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
136
|
+
mutation.assertWritable()
|
|
137
|
+
const result = await updateDocumentStatus({
|
|
138
|
+
data: {
|
|
139
|
+
expectedRevision: mutation.expectedRevision(),
|
|
140
|
+
collection: path,
|
|
141
|
+
id: String(initialData.id),
|
|
142
|
+
status,
|
|
143
|
+
},
|
|
125
144
|
})
|
|
145
|
+
mutation.adopt(result)
|
|
126
146
|
const description = t('collections.edit.statusChangedDescription', { status })
|
|
127
147
|
toastManager.add({
|
|
128
148
|
title: t('collections.edit.statusUpdateTitle', { label: singular }),
|
|
@@ -142,9 +162,16 @@ export const EditView = ({
|
|
|
142
162
|
search: (prev: Record<string, unknown>) => ({ ...prev }),
|
|
143
163
|
})
|
|
144
164
|
} catch (err) {
|
|
165
|
+
const handled = mutation.report(err)
|
|
166
|
+
if (handled === 'blocked') throw err
|
|
167
|
+
if (handled === 'committed') {
|
|
168
|
+
notifyCommittedHookFailure()
|
|
169
|
+
reloadDocument()
|
|
170
|
+
return
|
|
171
|
+
}
|
|
145
172
|
console.error('Status change error:', err)
|
|
146
173
|
const description = t('collections.edit.statusChangeFailedDescription', {
|
|
147
|
-
message: (
|
|
174
|
+
message: t('documentConcurrency.failed'),
|
|
148
175
|
})
|
|
149
176
|
toastManager.add({
|
|
150
177
|
title: t('collections.edit.statusUpdateTitle', { label: singular }),
|
|
@@ -170,14 +197,17 @@ export const EditView = ({
|
|
|
170
197
|
|
|
171
198
|
const handleSchedulePublication = async ({ publishAt }: { publishAt: string }) => {
|
|
172
199
|
try {
|
|
173
|
-
|
|
200
|
+
mutation.assertWritable()
|
|
201
|
+
const result = await scheduleCollectionDocumentPublish({
|
|
174
202
|
data: {
|
|
203
|
+
expectedRevision: mutation.expectedRevision(),
|
|
175
204
|
collection: path,
|
|
176
205
|
id: String(initialData.id),
|
|
177
206
|
publishAt,
|
|
178
207
|
expectedVersionId: String(initialData.versionId),
|
|
179
208
|
},
|
|
180
209
|
})
|
|
210
|
+
mutation.adopt(result)
|
|
181
211
|
toastManager.add({
|
|
182
212
|
title: t('scheduledPublication.toast.scheduledTitle'),
|
|
183
213
|
description: t('scheduledPublication.toast.scheduledDescription'),
|
|
@@ -185,9 +215,16 @@ export const EditView = ({
|
|
|
185
215
|
})
|
|
186
216
|
reloadDocument()
|
|
187
217
|
} catch (err) {
|
|
218
|
+
const handled = mutation.report(err)
|
|
219
|
+
if (handled === 'blocked') throw err
|
|
220
|
+
if (handled === 'committed') {
|
|
221
|
+
notifyCommittedHookFailure()
|
|
222
|
+
reloadDocument()
|
|
223
|
+
return
|
|
224
|
+
}
|
|
188
225
|
toastManager.add({
|
|
189
226
|
title: t('scheduledPublication.toast.failedTitle'),
|
|
190
|
-
description: (
|
|
227
|
+
description: t('documentConcurrency.failed'),
|
|
191
228
|
data: { intent: 'danger', iconType: 'danger', icon: true, close: true },
|
|
192
229
|
})
|
|
193
230
|
throw err
|
|
@@ -196,13 +233,16 @@ export const EditView = ({
|
|
|
196
233
|
|
|
197
234
|
const handleConfirmScheduledPublication = async () => {
|
|
198
235
|
try {
|
|
199
|
-
|
|
236
|
+
mutation.assertWritable()
|
|
237
|
+
const result = await confirmCollectionDocumentScheduledPublish({
|
|
200
238
|
data: {
|
|
239
|
+
expectedRevision: mutation.expectedRevision(),
|
|
201
240
|
collection: path,
|
|
202
241
|
id: String(initialData.id),
|
|
203
242
|
expectedVersionId: String(initialData.versionId),
|
|
204
243
|
},
|
|
205
244
|
})
|
|
245
|
+
mutation.adopt(result)
|
|
206
246
|
toastManager.add({
|
|
207
247
|
title: t('scheduledPublication.toast.confirmedTitle'),
|
|
208
248
|
description: t('scheduledPublication.toast.confirmedDescription'),
|
|
@@ -210,9 +250,16 @@ export const EditView = ({
|
|
|
210
250
|
})
|
|
211
251
|
reloadDocument()
|
|
212
252
|
} catch (err) {
|
|
253
|
+
const handled = mutation.report(err)
|
|
254
|
+
if (handled === 'blocked') throw err
|
|
255
|
+
if (handled === 'committed') {
|
|
256
|
+
notifyCommittedHookFailure()
|
|
257
|
+
reloadDocument()
|
|
258
|
+
return
|
|
259
|
+
}
|
|
213
260
|
toastManager.add({
|
|
214
261
|
title: t('scheduledPublication.toast.failedTitle'),
|
|
215
|
-
description: (
|
|
262
|
+
description: t('documentConcurrency.failed'),
|
|
216
263
|
data: { intent: 'danger', iconType: 'danger', icon: true, close: true },
|
|
217
264
|
})
|
|
218
265
|
throw err
|
|
@@ -221,9 +268,15 @@ export const EditView = ({
|
|
|
221
268
|
|
|
222
269
|
const handleCancelScheduledPublication = async () => {
|
|
223
270
|
try {
|
|
271
|
+
mutation.assertWritable()
|
|
224
272
|
const result = await cancelCollectionDocumentScheduledPublish({
|
|
225
|
-
data: {
|
|
273
|
+
data: {
|
|
274
|
+
expectedRevision: mutation.expectedRevision(),
|
|
275
|
+
collection: path,
|
|
276
|
+
id: String(initialData.id),
|
|
277
|
+
},
|
|
226
278
|
})
|
|
279
|
+
mutation.adopt({ ...result, documentId: String(initialData.id) })
|
|
227
280
|
const cancelled = result.status === 'cancelled'
|
|
228
281
|
toastManager.add({
|
|
229
282
|
title: cancelled
|
|
@@ -241,9 +294,16 @@ export const EditView = ({
|
|
|
241
294
|
})
|
|
242
295
|
reloadDocument()
|
|
243
296
|
} catch (err) {
|
|
297
|
+
const handled = mutation.report(err)
|
|
298
|
+
if (handled === 'blocked') throw err
|
|
299
|
+
if (handled === 'committed') {
|
|
300
|
+
notifyCommittedHookFailure()
|
|
301
|
+
reloadDocument()
|
|
302
|
+
return
|
|
303
|
+
}
|
|
244
304
|
toastManager.add({
|
|
245
305
|
title: t('scheduledPublication.toast.failedTitle'),
|
|
246
|
-
description: (
|
|
306
|
+
description: t('documentConcurrency.failed'),
|
|
247
307
|
data: { intent: 'danger', iconType: 'danger', icon: true, close: true },
|
|
248
308
|
})
|
|
249
309
|
throw err
|
|
@@ -264,7 +324,15 @@ export const EditView = ({
|
|
|
264
324
|
|
|
265
325
|
const handleUnpublish = async () => {
|
|
266
326
|
try {
|
|
267
|
-
|
|
327
|
+
mutation.assertWritable()
|
|
328
|
+
const result = await unpublishDocument({
|
|
329
|
+
data: {
|
|
330
|
+
expectedRevision: mutation.expectedRevision(),
|
|
331
|
+
collection: path,
|
|
332
|
+
id: String(initialData.id),
|
|
333
|
+
},
|
|
334
|
+
})
|
|
335
|
+
mutation.adopt(result)
|
|
268
336
|
const description = t('collections.edit.unpublishedDescription')
|
|
269
337
|
toastManager.add({
|
|
270
338
|
title: t('collections.edit.unpublishTitle', { label: singular }),
|
|
@@ -283,9 +351,16 @@ export const EditView = ({
|
|
|
283
351
|
search: (prev: Record<string, unknown>) => ({ ...prev }),
|
|
284
352
|
})
|
|
285
353
|
} catch (err) {
|
|
354
|
+
const handled = mutation.report(err)
|
|
355
|
+
if (handled === 'blocked') throw err
|
|
356
|
+
if (handled === 'committed') {
|
|
357
|
+
notifyCommittedHookFailure()
|
|
358
|
+
reloadDocument()
|
|
359
|
+
return
|
|
360
|
+
}
|
|
286
361
|
console.error('Unpublish error:', err)
|
|
287
362
|
const description = t('collections.edit.unpublishFailedDescription', {
|
|
288
|
-
message: (
|
|
363
|
+
message: t('documentConcurrency.failed'),
|
|
289
364
|
})
|
|
290
365
|
toastManager.add({
|
|
291
366
|
title: t('collections.edit.unpublishTitle', { label: singular }),
|
|
@@ -303,9 +378,23 @@ export const EditView = ({
|
|
|
303
378
|
|
|
304
379
|
const handleDuplicate = async () => {
|
|
305
380
|
try {
|
|
381
|
+
mutation.assertWritable()
|
|
306
382
|
const result = await duplicateCollectionDocument({
|
|
307
|
-
data: {
|
|
383
|
+
data: {
|
|
384
|
+
expectedRevision: mutation.expectedRevision(),
|
|
385
|
+
collection: path,
|
|
386
|
+
id: String(initialData.id),
|
|
387
|
+
},
|
|
308
388
|
})
|
|
389
|
+
if (hasCommittedDocumentHookFailure(result)) {
|
|
390
|
+
notifyCommittedHookFailure()
|
|
391
|
+
navigate({
|
|
392
|
+
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
393
|
+
params: { collection: path, id: result.documentId },
|
|
394
|
+
search: (prev: Record<string, unknown>) => ({ ...prev }),
|
|
395
|
+
})
|
|
396
|
+
return
|
|
397
|
+
}
|
|
309
398
|
const description = result.pathRetried
|
|
310
399
|
? t('collections.edit.duplicatedAutoPathDescription', { path: result.newPath })
|
|
311
400
|
: t('collections.edit.duplicatedPathDescription', { path: result.newPath })
|
|
@@ -332,9 +421,16 @@ export const EditView = ({
|
|
|
332
421
|
search: (prev: Record<string, unknown>) => ({ ...prev }),
|
|
333
422
|
})
|
|
334
423
|
} catch (err) {
|
|
424
|
+
const handled = mutation.report(err)
|
|
425
|
+
if (handled === 'blocked') throw err
|
|
426
|
+
if (handled === 'committed') {
|
|
427
|
+
notifyCommittedHookFailure()
|
|
428
|
+
reloadDocument()
|
|
429
|
+
return
|
|
430
|
+
}
|
|
335
431
|
console.error('Duplicate error:', err)
|
|
336
432
|
const description = t('collections.edit.duplicateFailedDescription', {
|
|
337
|
-
message: (
|
|
433
|
+
message: t('documentConcurrency.failed'),
|
|
338
434
|
})
|
|
339
435
|
toastManager.add({
|
|
340
436
|
title: t('collections.edit.duplicateTitle', { label: singular }),
|
|
@@ -358,8 +454,10 @@ export const EditView = ({
|
|
|
358
454
|
overwrite: boolean
|
|
359
455
|
}) => {
|
|
360
456
|
try {
|
|
457
|
+
mutation.assertWritable()
|
|
361
458
|
const result = await copyDocumentToLocale({
|
|
362
459
|
data: {
|
|
460
|
+
expectedRevision: mutation.expectedRevision(),
|
|
363
461
|
collection: path,
|
|
364
462
|
id: String(initialData.id),
|
|
365
463
|
sourceLocale: locale ?? defaultContentLocale,
|
|
@@ -367,6 +465,17 @@ export const EditView = ({
|
|
|
367
465
|
overwrite,
|
|
368
466
|
},
|
|
369
467
|
})
|
|
468
|
+
mutation.adopt(result)
|
|
469
|
+
if (hasCommittedDocumentHookFailure(result)) {
|
|
470
|
+
notifyCommittedHookFailure()
|
|
471
|
+
notifyScheduleSuspended()
|
|
472
|
+
navigate({
|
|
473
|
+
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
474
|
+
params: { collection: path, id: String(initialData.id) },
|
|
475
|
+
search: (prev: Record<string, unknown>) => ({ ...prev, locale: targetLocale }),
|
|
476
|
+
})
|
|
477
|
+
return
|
|
478
|
+
}
|
|
370
479
|
const sourceLabel =
|
|
371
480
|
contentLocales.find((l) => l.code === result.sourceLocale)?.label ?? result.sourceLocale
|
|
372
481
|
const targetLabel =
|
|
@@ -408,9 +517,16 @@ export const EditView = ({
|
|
|
408
517
|
search: (prev: Record<string, unknown>) => ({ ...prev, locale: targetLocale }),
|
|
409
518
|
})
|
|
410
519
|
} catch (err) {
|
|
520
|
+
const handled = mutation.report(err)
|
|
521
|
+
if (handled === 'blocked') throw err
|
|
522
|
+
if (handled === 'committed') {
|
|
523
|
+
notifyCommittedHookFailure()
|
|
524
|
+
reloadDocument()
|
|
525
|
+
return
|
|
526
|
+
}
|
|
411
527
|
console.error('Copy to locale error:', err)
|
|
412
528
|
const description = t('collections.edit.copyFailedDescription', {
|
|
413
|
-
message: (
|
|
529
|
+
message: t('documentConcurrency.failed'),
|
|
414
530
|
})
|
|
415
531
|
toastManager.add({
|
|
416
532
|
title: t('collections.edit.copyToLocaleTitle', { label: singular }),
|
|
@@ -428,13 +544,26 @@ export const EditView = ({
|
|
|
428
544
|
|
|
429
545
|
const handleDeleteLocale = async ({ targetLocale }: { targetLocale: string }) => {
|
|
430
546
|
try {
|
|
547
|
+
mutation.assertWritable()
|
|
431
548
|
const result = await deleteDocumentLocale({
|
|
432
549
|
data: {
|
|
550
|
+
expectedRevision: mutation.expectedRevision(),
|
|
433
551
|
collection: path,
|
|
434
552
|
id: String(initialData.id),
|
|
435
553
|
locale: targetLocale,
|
|
436
554
|
},
|
|
437
555
|
})
|
|
556
|
+
mutation.adopt(result)
|
|
557
|
+
if (hasCommittedDocumentHookFailure(result)) {
|
|
558
|
+
notifyCommittedHookFailure()
|
|
559
|
+
notifyScheduleSuspended()
|
|
560
|
+
navigate({
|
|
561
|
+
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
562
|
+
params: { collection: path, id: String(initialData.id) },
|
|
563
|
+
search: (prev: Record<string, unknown>) => ({ ...prev, locale: defaultContentLocale }),
|
|
564
|
+
})
|
|
565
|
+
return
|
|
566
|
+
}
|
|
438
567
|
const targetLabel =
|
|
439
568
|
contentLocales.find((l) => l.code === result.locale)?.label ?? result.locale
|
|
440
569
|
const description = t('collections.edit.deletedLocaleDescription', {
|
|
@@ -461,9 +590,16 @@ export const EditView = ({
|
|
|
461
590
|
search: (prev: Record<string, unknown>) => ({ ...prev, locale: defaultContentLocale }),
|
|
462
591
|
})
|
|
463
592
|
} catch (err) {
|
|
593
|
+
const handled = mutation.report(err)
|
|
594
|
+
if (handled === 'blocked') throw err
|
|
595
|
+
if (handled === 'committed') {
|
|
596
|
+
notifyCommittedHookFailure()
|
|
597
|
+
reloadDocument()
|
|
598
|
+
return
|
|
599
|
+
}
|
|
464
600
|
console.error('Delete locale error:', err)
|
|
465
601
|
const description = t('collections.edit.deleteLocaleFailedDescription', {
|
|
466
|
-
message: (
|
|
602
|
+
message: t('documentConcurrency.failed'),
|
|
467
603
|
})
|
|
468
604
|
toastManager.add({
|
|
469
605
|
title: t('collections.edit.deleteLocaleTitle', { label: singular }),
|
|
@@ -481,9 +617,28 @@ export const EditView = ({
|
|
|
481
617
|
|
|
482
618
|
const handleDelete = async () => {
|
|
483
619
|
try {
|
|
620
|
+
mutation.assertWritable()
|
|
484
621
|
const result = await deleteDocument({
|
|
485
|
-
data: {
|
|
622
|
+
data: {
|
|
623
|
+
expectedRevision: mutation.expectedRevision(),
|
|
624
|
+
collection: path,
|
|
625
|
+
id: String(initialData.id),
|
|
626
|
+
},
|
|
486
627
|
})
|
|
628
|
+
if (result.scheduledPublicationsNeedReconfirmation) {
|
|
629
|
+
toastManager.add({
|
|
630
|
+
title: t('documentConcurrency.schedulesTitle'),
|
|
631
|
+
description: (
|
|
632
|
+
<>
|
|
633
|
+
<p>{t('documentConcurrency.schedules')}</p>
|
|
634
|
+
<a href={getAdminRoutePath('scheduled-publications')}>
|
|
635
|
+
{t('documentConcurrency.reviewSchedules')}
|
|
636
|
+
</a>
|
|
637
|
+
</>
|
|
638
|
+
),
|
|
639
|
+
data: { intent: 'warning', iconType: 'warning', icon: true, close: true },
|
|
640
|
+
})
|
|
641
|
+
}
|
|
487
642
|
const hasSideEffectFailures = hasDeleteSideEffectFailures(result)
|
|
488
643
|
const description = hasSideEffectFailures
|
|
489
644
|
? t('collections.edit.deletedWithWarningsDescription', { label: singular })
|
|
@@ -498,7 +653,10 @@ export const EditView = ({
|
|
|
498
653
|
close: true,
|
|
499
654
|
},
|
|
500
655
|
})
|
|
501
|
-
setEditState({
|
|
656
|
+
setEditState({
|
|
657
|
+
status: hasSideEffectFailures ? 'warning' : 'success',
|
|
658
|
+
message: description,
|
|
659
|
+
})
|
|
502
660
|
// Navigate back to the collection list after deletion. The return
|
|
503
661
|
// target has now been consumed — clear the stored fallback so a later
|
|
504
662
|
// visit doesn't resurrect a stale list position.
|
|
@@ -509,9 +667,16 @@ export const EditView = ({
|
|
|
509
667
|
search: returnSearch,
|
|
510
668
|
})
|
|
511
669
|
} catch (err) {
|
|
670
|
+
const handled = mutation.report(err)
|
|
671
|
+
if (handled === 'blocked') throw err
|
|
672
|
+
if (handled === 'committed') {
|
|
673
|
+
notifyCommittedHookFailure()
|
|
674
|
+
reloadDocument()
|
|
675
|
+
return
|
|
676
|
+
}
|
|
512
677
|
console.error('Delete error:', err)
|
|
513
678
|
const description = t('collections.edit.deleteFailedDescription', {
|
|
514
|
-
message: (
|
|
679
|
+
message: t('documentConcurrency.failed'),
|
|
515
680
|
})
|
|
516
681
|
toastManager.add({
|
|
517
682
|
title: t('collections.edit.deleteTitle', { label: singular }),
|
|
@@ -550,65 +715,65 @@ export const EditView = ({
|
|
|
550
715
|
systemAvailableLocales?: string[]
|
|
551
716
|
}) => {
|
|
552
717
|
try {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
// fields changed, so a path/advertising edit never creates an empty
|
|
572
|
-
// content version.
|
|
573
|
-
if (contentDirty) {
|
|
574
|
-
await updateCollectionDocumentWithPatches({
|
|
575
|
-
data: {
|
|
576
|
-
collection: path,
|
|
577
|
-
id: String(initialData.id),
|
|
578
|
-
patches,
|
|
579
|
-
versionId: initialData.versionId as string | undefined,
|
|
580
|
-
locale,
|
|
581
|
-
},
|
|
582
|
-
})
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
if (contentDirty && scheduledPublication?.state === 'armed') {
|
|
718
|
+
mutation.assertWritable()
|
|
719
|
+
const result = await updateCollectionDocumentWithPatches({
|
|
720
|
+
data: {
|
|
721
|
+
collection: path,
|
|
722
|
+
id: String(initialData.id),
|
|
723
|
+
expectedRevision: mutation.expectedRevision(),
|
|
724
|
+
patches: contentDirty ? patches : [],
|
|
725
|
+
locale,
|
|
726
|
+
...(pathDirty ? { path: systemPath ?? null } : {}),
|
|
727
|
+
...(availableLocalesDirty ? { availableLocales: systemAvailableLocales ?? [] } : {}),
|
|
728
|
+
},
|
|
729
|
+
})
|
|
730
|
+
mutation.adopt(result)
|
|
731
|
+
const hookFailed = hasCommittedDocumentHookFailure(result)
|
|
732
|
+
if (
|
|
733
|
+
(contentDirty || pathDirty || availableLocalesDirty) &&
|
|
734
|
+
scheduledPublication?.state === 'armed'
|
|
735
|
+
) {
|
|
586
736
|
notifyScheduleSuspended()
|
|
587
737
|
}
|
|
588
738
|
|
|
589
|
-
const description =
|
|
739
|
+
const description = hookFailed
|
|
740
|
+
? t('collections.save.hookFailedDescription')
|
|
741
|
+
: t('collections.edit.updatedDescription', { label: singularLower })
|
|
590
742
|
toastManager.add({
|
|
591
|
-
title:
|
|
743
|
+
title: hookFailed
|
|
744
|
+
? t('collections.save.hookFailedToast')
|
|
745
|
+
: t('collections.edit.updateTitle', { label: singular }),
|
|
592
746
|
description,
|
|
593
747
|
data: {
|
|
594
|
-
intent: 'success',
|
|
595
|
-
iconType: 'success',
|
|
748
|
+
intent: hookFailed ? 'warning' : 'success',
|
|
749
|
+
iconType: hookFailed ? 'warning' : 'success',
|
|
596
750
|
icon: true,
|
|
597
751
|
close: true,
|
|
598
752
|
},
|
|
599
753
|
})
|
|
600
754
|
|
|
601
|
-
setEditState({ status: 'success', message: description })
|
|
755
|
+
setEditState({ status: hookFailed ? 'warning' : 'success', message: description })
|
|
602
756
|
|
|
603
757
|
// Re-navigate to the same route so the loader re-fetches the document.
|
|
604
|
-
// The new version will have a fresh version ID,
|
|
605
|
-
// updated publishedVersion metadata.
|
|
758
|
+
// The new version will have a fresh version ID, the collection's default
|
|
759
|
+
// workflow status, and updated publishedVersion metadata.
|
|
606
760
|
navigate({
|
|
607
761
|
to: getAdminRoutePath('collections', '$collection', '$id'),
|
|
608
762
|
params: { collection: path, id: String(initialData.id) },
|
|
609
763
|
search: (prev: Record<string, unknown>) => ({ ...prev }),
|
|
764
|
+
// Every write above has succeeded. FormRenderer clears its dirty
|
|
765
|
+
// baseline after this handler returns, so bypass the guard during the
|
|
766
|
+
// intentional post-save refresh rather than racing that state update.
|
|
767
|
+
ignoreBlocker: true,
|
|
610
768
|
})
|
|
611
769
|
} catch (err) {
|
|
770
|
+
const handled = mutation.report(err)
|
|
771
|
+
if (handled === 'blocked') throw err
|
|
772
|
+
if (handled === 'committed') {
|
|
773
|
+
notifyCommittedHookFailure()
|
|
774
|
+
reloadDocument()
|
|
775
|
+
return
|
|
776
|
+
}
|
|
612
777
|
console.error('Network error:', err)
|
|
613
778
|
const description = t('collections.edit.updateFailedDescription', { label: singularLower })
|
|
614
779
|
toastManager.add({
|
|
@@ -634,6 +799,13 @@ export const EditView = ({
|
|
|
634
799
|
<Section>
|
|
635
800
|
<Container>
|
|
636
801
|
<FormRenderer
|
|
802
|
+
mutationIssue={mutation.issue}
|
|
803
|
+
mutationsBlocked={mutation.blocked}
|
|
804
|
+
observedRevision={mutation.revision}
|
|
805
|
+
onMutationError={mutation.report}
|
|
806
|
+
onTreeMutationCommitted={mutation.adopt}
|
|
807
|
+
scheduledPublicationsNeedReconfirmation={mutation.scheduleNotice}
|
|
808
|
+
scheduledPublicationsHref={getAdminRoutePath('scheduled-publications')}
|
|
637
809
|
mode="edit"
|
|
638
810
|
fields={fields}
|
|
639
811
|
onSubmit={handleSubmit}
|
|
@@ -155,6 +155,7 @@ export const HistoryView = ({
|
|
|
155
155
|
currentVersionId,
|
|
156
156
|
restoreStatusLabel,
|
|
157
157
|
openCompare,
|
|
158
|
+
mutationsBlocked,
|
|
158
159
|
openRestore,
|
|
159
160
|
}) =>
|
|
160
161
|
columns.flatMap((column) => {
|
|
@@ -213,6 +214,7 @@ export const HistoryView = ({
|
|
|
213
214
|
variant="outlined"
|
|
214
215
|
size="xs"
|
|
215
216
|
intent="noeffect"
|
|
217
|
+
disabled={mutationsBlocked}
|
|
216
218
|
onClick={openRestore}
|
|
217
219
|
className={cx('byline-coll-history-restore-button', styles.restoreButton)}
|
|
218
220
|
title={t('collections.history.restoreButtonTitle', {
|
|
@@ -279,8 +281,15 @@ export const HistoryView = ({
|
|
|
279
281
|
rowPresentation={rowPresentation}
|
|
280
282
|
loadHistoricalVersion={getCollectionDocumentVersion}
|
|
281
283
|
onPageSizeChange={handlePageSizeChange}
|
|
282
|
-
restoreVersion={(versionId) =>
|
|
283
|
-
restoreDocumentVersion({
|
|
284
|
+
restoreVersion={(versionId, expectedRevision) =>
|
|
285
|
+
restoreDocumentVersion({
|
|
286
|
+
data: {
|
|
287
|
+
expectedRevision,
|
|
288
|
+
collection,
|
|
289
|
+
id,
|
|
290
|
+
versionId,
|
|
291
|
+
},
|
|
292
|
+
})
|
|
284
293
|
}
|
|
285
294
|
onRestoreComplete={async () => {
|
|
286
295
|
await router.invalidate()
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DocumentMutationNotice } from '../document-mutation-notice.js'
|
|
2
|
+
import { useDocumentMutationState } from '../document-mutation-state.js'
|
|
1
3
|
/**
|
|
2
4
|
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
5
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
@@ -178,6 +180,7 @@ export const ListView = ({
|
|
|
178
180
|
const navigate = useNavigate()
|
|
179
181
|
const router = useRouter()
|
|
180
182
|
const toastManager = useToastManager()
|
|
183
|
+
const mutation = useDocumentMutationState(undefined)
|
|
181
184
|
const { t } = useTranslation('byline-admin')
|
|
182
185
|
const location = useRouterState({ select: (s) => s.location })
|
|
183
186
|
|
|
@@ -191,10 +194,10 @@ export const ListView = ({
|
|
|
191
194
|
const [localDocs, setLocalDocs] = useState(data.docs)
|
|
192
195
|
const [isReordering, setIsReordering] = useState(false)
|
|
193
196
|
useEffect(() => {
|
|
194
|
-
if (!isReordering) {
|
|
197
|
+
if (!isReordering && !mutation.blocked) {
|
|
195
198
|
setLocalDocs(data.docs)
|
|
196
199
|
}
|
|
197
|
-
}, [data.docs, isReordering])
|
|
200
|
+
}, [data.docs, isReordering, mutation.blocked])
|
|
198
201
|
|
|
199
202
|
// Drag is only meaningful in the canonical view: the default order_key
|
|
200
203
|
// sort, no search, no status filter. Otherwise the visible order isn't
|
|
@@ -209,7 +212,7 @@ export const ListView = ({
|
|
|
209
212
|
}
|
|
210
213
|
const isCanonicalView =
|
|
211
214
|
!searchParams.order && !searchParams.desc && !searchParams.query && !searchParams.status
|
|
212
|
-
const dragEnabled = orderable && isCanonicalView && !!onReorder
|
|
215
|
+
const dragEnabled = orderable && isCanonicalView && !!onReorder && !mutation.blocked
|
|
213
216
|
|
|
214
217
|
// The *effective* sort for the header indicators: explicit URL params win;
|
|
215
218
|
// otherwise the server echoes a configured `defaultSort` (admin config)
|
|
@@ -226,7 +229,7 @@ export const ListView = ({
|
|
|
226
229
|
|
|
227
230
|
const handleDragEnd = async (event: DragEndEvent) => {
|
|
228
231
|
const { active, over } = event
|
|
229
|
-
if (!over || active.id === over.id || !onReorder) return
|
|
232
|
+
if (mutation.blocked || isReordering || !over || active.id === over.id || !onReorder) return
|
|
230
233
|
const oldIndex = localDocs.findIndex((d) => d.id === active.id)
|
|
231
234
|
const newIndex = localDocs.findIndex((d) => d.id === over.id)
|
|
232
235
|
if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) return
|
|
@@ -242,14 +245,26 @@ export const ListView = ({
|
|
|
242
245
|
const before = next[newIndex - 1]?.id ?? null
|
|
243
246
|
const after = next[newIndex + 1]?.id ?? null
|
|
244
247
|
try {
|
|
245
|
-
|
|
248
|
+
mutation.assertWritable()
|
|
249
|
+
const receipt = await onReorder({
|
|
246
250
|
documentId: String(active.id),
|
|
247
251
|
beforeDocumentId: before,
|
|
248
252
|
afterDocumentId: after,
|
|
249
253
|
})
|
|
250
|
-
|
|
254
|
+
mutation.reportStructure(receipt)
|
|
255
|
+
try {
|
|
256
|
+
await router.invalidate()
|
|
257
|
+
} catch {
|
|
258
|
+
// The mutation committed; a failed refresh must not undo its UI receipt.
|
|
259
|
+
toastManager.add({
|
|
260
|
+
title: t('collections.list.treeRefreshFailedToast'),
|
|
261
|
+
description: t('collections.list.treeRefreshFailedDescription'),
|
|
262
|
+
data: { intent: 'warning', iconType: 'warning', icon: true, close: true },
|
|
263
|
+
})
|
|
264
|
+
}
|
|
251
265
|
} catch (_err) {
|
|
252
266
|
setLocalDocs(previousDocs)
|
|
267
|
+
if (mutation.report(_err)) return
|
|
253
268
|
toastManager.add({
|
|
254
269
|
title: t('collections.list.reorderFailedToast'),
|
|
255
270
|
description: t('collections.list.reorderFailedDescription'),
|
|
@@ -359,6 +374,7 @@ export const ListView = ({
|
|
|
359
374
|
return (
|
|
360
375
|
<Section>
|
|
361
376
|
<Container>
|
|
377
|
+
<DocumentMutationNotice issue={mutation.issue} scheduleNotice={mutation.scheduleNotice} />
|
|
362
378
|
<div className={cx('byline-coll-list-head', styles.head)}>
|
|
363
379
|
<h1 className={cx('byline-coll-list-title', styles.title)}>
|
|
364
380
|
{data.included.collection.labels.plural as string}
|