@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.
Files changed (142) hide show
  1. package/dist/admin-shell/collections/edit.d.ts +1 -1
  2. package/dist/admin-shell/collections/edit.js +151 -31
  3. package/dist/admin-shell/collections/history.js +4 -2
  4. package/dist/admin-shell/collections/list.js +30 -6
  5. package/dist/admin-shell/collections/restore-version-modal.d.ts +3 -1
  6. package/dist/admin-shell/collections/restore-version-modal.js +9 -4
  7. package/dist/admin-shell/collections/tree-list.js +16 -6
  8. package/dist/admin-shell/collections/version-history.d.ts +2 -1
  9. package/dist/admin-shell/collections/version-history.js +11 -2
  10. package/dist/admin-shell/document-mutation-notice.d.ts +6 -0
  11. package/dist/admin-shell/document-mutation-notice.js +53 -0
  12. package/dist/admin-shell/document-mutation-state.d.ts +22 -0
  13. package/dist/admin-shell/document-mutation-state.js +71 -0
  14. package/dist/admin-shell/document-mutation-state.test.d.ts +1 -0
  15. package/dist/admin-shell/scheduled-publications/list.js +15 -5
  16. package/dist/admin-shell/singletons/history.js +4 -2
  17. package/dist/admin-shell/singletons/view.d.ts +1 -1
  18. package/dist/admin-shell/singletons/view.js +98 -19
  19. package/dist/integrations/byline-field-services.js +2 -9
  20. package/dist/routes/create-collection-edit-route.js +1 -1
  21. package/dist/routes/create-collection-list-route.d.ts +0 -7
  22. package/dist/routes/create-collection-list-route.js +7 -9
  23. package/dist/routes/create-singleton-route.d.ts +2 -0
  24. package/dist/routes/create-singleton-route.js +4 -3
  25. package/dist/server-fns/collections/copy-to-locale.d.ts +2 -7
  26. package/dist/server-fns/collections/copy-to-locale.js +4 -2
  27. package/dist/server-fns/collections/create.d.ts +1 -7
  28. package/dist/server-fns/collections/create.js +5 -3
  29. package/dist/server-fns/collections/delete-locale.d.ts +2 -7
  30. package/dist/server-fns/collections/delete-locale.js +4 -2
  31. package/dist/server-fns/collections/delete-outcome.d.ts +7 -3
  32. package/dist/server-fns/collections/delete-outcome.js +4 -0
  33. package/dist/server-fns/collections/delete.d.ts +2 -7
  34. package/dist/server-fns/collections/delete.js +5 -3
  35. package/dist/server-fns/collections/duplicate.d.ts +2 -7
  36. package/dist/server-fns/collections/duplicate.js +5 -3
  37. package/dist/server-fns/collections/get.d.ts +1 -7
  38. package/dist/server-fns/collections/get.js +5 -4
  39. package/dist/server-fns/collections/list.d.ts +0 -7
  40. package/dist/server-fns/collections/list.js +11 -3
  41. package/dist/server-fns/collections/reorder.d.ts +7 -8
  42. package/dist/server-fns/collections/reorder.js +20 -76
  43. package/dist/server-fns/collections/restore-version.d.ts +3 -7
  44. package/dist/server-fns/collections/restore-version.js +5 -2
  45. package/dist/server-fns/collections/save-outcome.d.ts +1 -0
  46. package/dist/server-fns/collections/save-outcome.js +1 -0
  47. package/dist/server-fns/collections/scheduled-publication.d.ts +49 -9
  48. package/dist/server-fns/collections/scheduled-publication.js +31 -21
  49. package/dist/server-fns/collections/status.d.ts +8 -7
  50. package/dist/server-fns/collections/status.js +13 -5
  51. package/dist/server-fns/collections/tree-mutation.d.ts +7 -6
  52. package/dist/server-fns/collections/tree-mutation.js +17 -5
  53. package/dist/server-fns/collections/tree.d.ts +14 -8
  54. package/dist/server-fns/collections/tree.js +21 -17
  55. package/dist/server-fns/collections/update.d.ts +12 -11
  56. package/dist/server-fns/collections/update.js +34 -20
  57. package/dist/server-fns/collections/upload.d.ts +1 -0
  58. package/dist/server-fns/collections/upload.js +3 -2
  59. package/dist/server-fns/document-mutation-errors.d.ts +5 -0
  60. package/dist/server-fns/document-mutation-errors.js +50 -0
  61. package/dist/server-fns/editable-revision.d.ts +9 -0
  62. package/dist/server-fns/editable-revision.js +11 -0
  63. package/dist/server-fns/singleton-document-read.d.ts +1 -1
  64. package/dist/server-fns/singleton-document-read.js +7 -3
  65. package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +6 -8
  66. package/dist/server-fns/singletons/cancel-scheduled-publish.js +4 -1
  67. package/dist/server-fns/singletons/change-status.d.ts +2 -7
  68. package/dist/server-fns/singletons/change-status.js +4 -1
  69. package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +5 -8
  70. package/dist/server-fns/singletons/confirm-scheduled-publish.js +4 -2
  71. package/dist/server-fns/singletons/copy-to-locale.d.ts +2 -7
  72. package/dist/server-fns/singletons/copy-to-locale.js +4 -2
  73. package/dist/server-fns/singletons/get.d.ts +4 -4
  74. package/dist/server-fns/singletons/restore-version.d.ts +2 -7
  75. package/dist/server-fns/singletons/restore-version.js +6 -3
  76. package/dist/server-fns/singletons/schedule-publish.d.ts +5 -8
  77. package/dist/server-fns/singletons/schedule-publish.js +4 -2
  78. package/dist/server-fns/singletons/unpublish.d.ts +2 -7
  79. package/dist/server-fns/singletons/unpublish.js +4 -1
  80. package/dist/server-fns/singletons/update.d.ts +4 -11
  81. package/dist/server-fns/singletons/update.js +5 -4
  82. package/dist/server-fns/structural-receipt.d.ts +4 -0
  83. package/dist/server-fns/structural-receipt.js +31 -0
  84. package/package.json +11 -11
  85. package/src/admin-shell/collections/edit.tsx +173 -54
  86. package/src/admin-shell/collections/history.tsx +11 -2
  87. package/src/admin-shell/collections/list.tsx +22 -6
  88. package/src/admin-shell/collections/restore-version-modal.tsx +12 -3
  89. package/src/admin-shell/collections/save-navigation.test.tsx +156 -8
  90. package/src/admin-shell/collections/tree-list.tsx +16 -6
  91. package/src/admin-shell/collections/version-history.tsx +13 -3
  92. package/src/admin-shell/document-mutation-notice.tsx +45 -0
  93. package/src/admin-shell/document-mutation-state.test.tsx +146 -0
  94. package/src/admin-shell/document-mutation-state.ts +95 -0
  95. package/src/admin-shell/scheduled-publications/list.tsx +16 -6
  96. package/src/admin-shell/singletons/history.test.tsx +3 -1
  97. package/src/admin-shell/singletons/history.tsx +10 -2
  98. package/src/admin-shell/singletons/view.test.tsx +59 -18
  99. package/src/admin-shell/singletons/view.tsx +110 -29
  100. package/src/integrations/byline-field-services.ts +2 -3
  101. package/src/routes/create-collection-edit-route.tsx +1 -0
  102. package/src/routes/create-collection-list-route.tsx +13 -2
  103. package/src/routes/create-singleton-route.test.node.ts +10 -2
  104. package/src/routes/create-singleton-route.tsx +6 -4
  105. package/src/server-fns/collections/copy-to-locale.ts +41 -36
  106. package/src/server-fns/collections/create.ts +54 -50
  107. package/src/server-fns/collections/delete-locale.ts +41 -32
  108. package/src/server-fns/collections/delete-outcome.test.node.ts +16 -0
  109. package/src/server-fns/collections/delete-outcome.ts +27 -16
  110. package/src/server-fns/collections/delete.ts +44 -38
  111. package/src/server-fns/collections/duplicate.ts +38 -32
  112. package/src/server-fns/collections/get.ts +8 -4
  113. package/src/server-fns/collections/list.ts +8 -3
  114. package/src/server-fns/collections/reorder.ts +32 -124
  115. package/src/server-fns/collections/restore-version.ts +47 -39
  116. package/src/server-fns/collections/revision-input.test.node.ts +108 -0
  117. package/src/server-fns/collections/save-outcome.test.node.ts +3 -0
  118. package/src/server-fns/collections/save-outcome.ts +2 -0
  119. package/src/server-fns/collections/scheduled-publication.ts +138 -111
  120. package/src/server-fns/collections/status.ts +86 -64
  121. package/src/server-fns/collections/tree-mutation.test.node.ts +87 -29
  122. package/src/server-fns/collections/tree-mutation.ts +28 -11
  123. package/src/server-fns/collections/tree.ts +150 -122
  124. package/src/server-fns/collections/update.ts +110 -86
  125. package/src/server-fns/collections/upload-singleton.test.node.ts +3 -7
  126. package/src/server-fns/collections/upload.ts +81 -77
  127. package/src/server-fns/document-mutation-errors.ts +67 -0
  128. package/src/server-fns/document-mutation-transport.test.node.ts +211 -0
  129. package/src/server-fns/editable-revision.test.node.ts +23 -0
  130. package/src/server-fns/editable-revision.ts +18 -0
  131. package/src/server-fns/singleton-document-read.ts +7 -3
  132. package/src/server-fns/singletons/cancel-scheduled-publish.ts +10 -3
  133. package/src/server-fns/singletons/change-status.ts +10 -3
  134. package/src/server-fns/singletons/confirm-scheduled-publish.ts +12 -6
  135. package/src/server-fns/singletons/copy-to-locale.ts +20 -15
  136. package/src/server-fns/singletons/get.ts +3 -3
  137. package/src/server-fns/singletons/restore-version.ts +17 -12
  138. package/src/server-fns/singletons/schedule-publish.ts +18 -7
  139. package/src/server-fns/singletons/singletons.test.node.ts +63 -25
  140. package/src/server-fns/singletons/unpublish.ts +10 -3
  141. package/src/server-fns/singletons/update.ts +21 -16
  142. package/src/server-fns/structural-receipt.ts +41 -0
@@ -8,7 +8,7 @@
8
8
  import type { CollectionAdminConfig, MultiCollectionDefinition } from '@byline/core';
9
9
  import type { AnyCollectionSchemaTypes } from '@byline/core/zod-schemas';
10
10
  import type { ContentLocaleOption } from './view-menu.js';
11
- export declare const EditView: ({ collectionDefinition, adminConfig, initialData, locale, contentLocales, defaultContentLocale, returnSearch, }: {
11
+ export declare const EditView: ({ collectionDefinition, adminConfig, initialData: loadedData, locale, contentLocales, defaultContentLocale, returnSearch, }: {
12
12
  collectionDefinition: MultiCollectionDefinition;
13
13
  adminConfig?: CollectionAdminConfig;
14
14
  initialData: AnyCollectionSchemaTypes['UpdateType'];
@@ -1,4 +1,4 @@
1
- import { jsx } from "react/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { FormRenderer } from "@byline/admin/react";
4
4
  import { getDefaultStatus, getWorkflow, getWorkflowStatuses, validateStatusTransition } from "@byline/core";
@@ -6,11 +6,14 @@ import { useTranslation } from "@byline/i18n/react";
6
6
  import { Container, Section, useToastManager } from "@byline/ui/react";
7
7
  import { getAdminRoutePath } from "../../routes/admin-path.js";
8
8
  import { clearListReturnState } from "../../routes/list-return-storage.js";
9
- import { cancelCollectionDocumentScheduledPublish, confirmCollectionDocumentScheduledPublish, copyDocumentToLocale, deleteDocument, deleteDocumentLocale, duplicateCollectionDocument, hasCommittedDocumentHookFailure, hasDeleteSideEffectFailures, scheduleCollectionDocumentPublish, unpublishDocument, updateCollectionDocumentSystemFields, updateCollectionDocumentWithPatches, updateDocumentStatus } from "../../server-fns/collections/index.js";
9
+ import { cancelCollectionDocumentScheduledPublish, confirmCollectionDocumentScheduledPublish, copyDocumentToLocale, deleteDocument, deleteDocumentLocale, duplicateCollectionDocument, hasCommittedDocumentHookFailure, hasDeleteSideEffectFailures, scheduleCollectionDocumentPublish, unpublishDocument, updateCollectionDocumentWithPatches, updateDocumentStatus } from "../../server-fns/collections/index.js";
10
10
  import { useNavigate } from "../chrome/loose-router.js";
11
+ import { useDocumentMutationState } from "../document-mutation-state.js";
11
12
  import { useTanStackNavigationGuard } from "./tanstack-navigation-guard.js";
12
13
  import { ViewMenu } from "./view-menu.js";
13
- const EditView = ({ collectionDefinition, adminConfig, initialData, locale, contentLocales, defaultContentLocale, returnSearch })=>{
14
+ const EditView = ({ collectionDefinition, adminConfig, initialData: loadedData, locale, contentLocales, defaultContentLocale, returnSearch })=>{
15
+ const mutation = useDocumentMutationState(loadedData);
16
+ const initialData = mutation.document ?? loadedData;
14
17
  const toastManager = useToastManager();
15
18
  const { t } = useTranslation('byline-admin');
16
19
  const [_editState, setEditState] = useState({
@@ -75,13 +78,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
75
78
  };
76
79
  const handleStatusChange = async (status)=>{
77
80
  try {
78
- await updateDocumentStatus({
81
+ mutation.assertWritable();
82
+ const result = await updateDocumentStatus({
79
83
  data: {
84
+ expectedRevision: mutation.expectedRevision(),
80
85
  collection: path,
81
86
  id: String(initialData.id),
82
87
  status
83
88
  }
84
89
  });
90
+ mutation.adopt(result);
85
91
  const description = t("collections.edit.statusChangedDescription", {
86
92
  status
87
93
  });
@@ -112,9 +118,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
112
118
  })
113
119
  });
114
120
  } catch (err) {
121
+ const handled = mutation.report(err);
122
+ if ('blocked' === handled) throw err;
123
+ if ('committed' === handled) {
124
+ notifyCommittedHookFailure();
125
+ reloadDocument();
126
+ return;
127
+ }
115
128
  console.error('Status change error:', err);
116
129
  const description = t("collections.edit.statusChangeFailedDescription", {
117
- message: err.message
130
+ message: t('documentConcurrency.failed')
118
131
  });
119
132
  toastManager.add({
120
133
  title: t('collections.edit.statusUpdateTitle', {
@@ -148,14 +161,17 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
148
161
  };
149
162
  const handleSchedulePublication = async ({ publishAt })=>{
150
163
  try {
151
- await scheduleCollectionDocumentPublish({
164
+ mutation.assertWritable();
165
+ const result = await scheduleCollectionDocumentPublish({
152
166
  data: {
167
+ expectedRevision: mutation.expectedRevision(),
153
168
  collection: path,
154
169
  id: String(initialData.id),
155
170
  publishAt,
156
171
  expectedVersionId: String(initialData.versionId)
157
172
  }
158
173
  });
174
+ mutation.adopt(result);
159
175
  toastManager.add({
160
176
  title: t('scheduledPublication.toast.scheduledTitle'),
161
177
  description: t("scheduledPublication.toast.scheduledDescription"),
@@ -168,9 +184,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
168
184
  });
169
185
  reloadDocument();
170
186
  } catch (err) {
187
+ const handled = mutation.report(err);
188
+ if ('blocked' === handled) throw err;
189
+ if ('committed' === handled) {
190
+ notifyCommittedHookFailure();
191
+ reloadDocument();
192
+ return;
193
+ }
171
194
  toastManager.add({
172
195
  title: t('scheduledPublication.toast.failedTitle'),
173
- description: err.message,
196
+ description: t('documentConcurrency.failed'),
174
197
  data: {
175
198
  intent: 'danger',
176
199
  iconType: 'danger',
@@ -183,13 +206,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
183
206
  };
184
207
  const handleConfirmScheduledPublication = async ()=>{
185
208
  try {
186
- await confirmCollectionDocumentScheduledPublish({
209
+ mutation.assertWritable();
210
+ const result = await confirmCollectionDocumentScheduledPublish({
187
211
  data: {
212
+ expectedRevision: mutation.expectedRevision(),
188
213
  collection: path,
189
214
  id: String(initialData.id),
190
215
  expectedVersionId: String(initialData.versionId)
191
216
  }
192
217
  });
218
+ mutation.adopt(result);
193
219
  toastManager.add({
194
220
  title: t('scheduledPublication.toast.confirmedTitle'),
195
221
  description: t("scheduledPublication.toast.confirmedDescription"),
@@ -202,9 +228,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
202
228
  });
203
229
  reloadDocument();
204
230
  } catch (err) {
231
+ const handled = mutation.report(err);
232
+ if ('blocked' === handled) throw err;
233
+ if ('committed' === handled) {
234
+ notifyCommittedHookFailure();
235
+ reloadDocument();
236
+ return;
237
+ }
205
238
  toastManager.add({
206
239
  title: t('scheduledPublication.toast.failedTitle'),
207
- description: err.message,
240
+ description: t('documentConcurrency.failed'),
208
241
  data: {
209
242
  intent: 'danger',
210
243
  iconType: 'danger',
@@ -217,12 +250,18 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
217
250
  };
218
251
  const handleCancelScheduledPublication = async ()=>{
219
252
  try {
253
+ mutation.assertWritable();
220
254
  const result = await cancelCollectionDocumentScheduledPublish({
221
255
  data: {
256
+ expectedRevision: mutation.expectedRevision(),
222
257
  collection: path,
223
258
  id: String(initialData.id)
224
259
  }
225
260
  });
261
+ mutation.adopt({
262
+ ...result,
263
+ documentId: String(initialData.id)
264
+ });
226
265
  const cancelled = 'cancelled' === result.status;
227
266
  toastManager.add({
228
267
  title: cancelled ? t('scheduledPublication.toast.cancelledTitle') : t('scheduledPublication.toast.cancelLostTitle'),
@@ -236,9 +275,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
236
275
  });
237
276
  reloadDocument();
238
277
  } catch (err) {
278
+ const handled = mutation.report(err);
279
+ if ('blocked' === handled) throw err;
280
+ if ('committed' === handled) {
281
+ notifyCommittedHookFailure();
282
+ reloadDocument();
283
+ return;
284
+ }
239
285
  toastManager.add({
240
286
  title: t('scheduledPublication.toast.failedTitle'),
241
- description: err.message,
287
+ description: t('documentConcurrency.failed'),
242
288
  data: {
243
289
  intent: 'danger',
244
290
  iconType: 'danger',
@@ -253,12 +299,15 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
253
299
  const restoreWarnings = initialData?._restoreWarnings;
254
300
  const handleUnpublish = async ()=>{
255
301
  try {
256
- await unpublishDocument({
302
+ mutation.assertWritable();
303
+ const result = await unpublishDocument({
257
304
  data: {
305
+ expectedRevision: mutation.expectedRevision(),
258
306
  collection: path,
259
307
  id: String(initialData.id)
260
308
  }
261
309
  });
310
+ mutation.adopt(result);
262
311
  const description = t("collections.edit.unpublishedDescription");
263
312
  toastManager.add({
264
313
  title: t('collections.edit.unpublishTitle', {
@@ -287,9 +336,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
287
336
  })
288
337
  });
289
338
  } catch (err) {
339
+ const handled = mutation.report(err);
340
+ if ('blocked' === handled) throw err;
341
+ if ('committed' === handled) {
342
+ notifyCommittedHookFailure();
343
+ reloadDocument();
344
+ return;
345
+ }
290
346
  console.error('Unpublish error:', err);
291
347
  const description = t("collections.edit.unpublishFailedDescription", {
292
- message: err.message
348
+ message: t('documentConcurrency.failed')
293
349
  });
294
350
  toastManager.add({
295
351
  title: t('collections.edit.unpublishTitle', {
@@ -311,8 +367,10 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
311
367
  };
312
368
  const handleDuplicate = async ()=>{
313
369
  try {
370
+ mutation.assertWritable();
314
371
  const result = await duplicateCollectionDocument({
315
372
  data: {
373
+ expectedRevision: mutation.expectedRevision(),
316
374
  collection: path,
317
375
  id: String(initialData.id)
318
376
  }
@@ -365,9 +423,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
365
423
  })
366
424
  });
367
425
  } catch (err) {
426
+ const handled = mutation.report(err);
427
+ if ('blocked' === handled) throw err;
428
+ if ('committed' === handled) {
429
+ notifyCommittedHookFailure();
430
+ reloadDocument();
431
+ return;
432
+ }
368
433
  console.error('Duplicate error:', err);
369
434
  const description = t("collections.edit.duplicateFailedDescription", {
370
- message: err.message
435
+ message: t('documentConcurrency.failed')
371
436
  });
372
437
  toastManager.add({
373
438
  title: t('collections.edit.duplicateTitle', {
@@ -389,8 +454,10 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
389
454
  };
390
455
  const handleCopyToLocale = async ({ targetLocale, overwrite })=>{
391
456
  try {
457
+ mutation.assertWritable();
392
458
  const result = await copyDocumentToLocale({
393
459
  data: {
460
+ expectedRevision: mutation.expectedRevision(),
394
461
  collection: path,
395
462
  id: String(initialData.id),
396
463
  sourceLocale: locale ?? defaultContentLocale,
@@ -398,6 +465,7 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
398
465
  overwrite
399
466
  }
400
467
  });
468
+ mutation.adopt(result);
401
469
  if (hasCommittedDocumentHookFailure(result)) {
402
470
  notifyCommittedHookFailure();
403
471
  notifyScheduleSuspended();
@@ -456,9 +524,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
456
524
  })
457
525
  });
458
526
  } catch (err) {
527
+ const handled = mutation.report(err);
528
+ if ('blocked' === handled) throw err;
529
+ if ('committed' === handled) {
530
+ notifyCommittedHookFailure();
531
+ reloadDocument();
532
+ return;
533
+ }
459
534
  console.error('Copy to locale error:', err);
460
535
  const description = t("collections.edit.copyFailedDescription", {
461
- message: err.message
536
+ message: t('documentConcurrency.failed')
462
537
  });
463
538
  toastManager.add({
464
539
  title: t('collections.edit.copyToLocaleTitle', {
@@ -480,13 +555,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
480
555
  };
481
556
  const handleDeleteLocale = async ({ targetLocale })=>{
482
557
  try {
558
+ mutation.assertWritable();
483
559
  const result = await deleteDocumentLocale({
484
560
  data: {
561
+ expectedRevision: mutation.expectedRevision(),
485
562
  collection: path,
486
563
  id: String(initialData.id),
487
564
  locale: targetLocale
488
565
  }
489
566
  });
567
+ mutation.adopt(result);
490
568
  if (hasCommittedDocumentHookFailure(result)) {
491
569
  notifyCommittedHookFailure();
492
570
  notifyScheduleSuspended();
@@ -537,9 +615,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
537
615
  })
538
616
  });
539
617
  } catch (err) {
618
+ const handled = mutation.report(err);
619
+ if ('blocked' === handled) throw err;
620
+ if ('committed' === handled) {
621
+ notifyCommittedHookFailure();
622
+ reloadDocument();
623
+ return;
624
+ }
540
625
  console.error('Delete locale error:', err);
541
626
  const description = t("collections.edit.deleteLocaleFailedDescription", {
542
- message: err.message
627
+ message: t('documentConcurrency.failed')
543
628
  });
544
629
  toastManager.add({
545
630
  title: t('collections.edit.deleteLocaleTitle', {
@@ -561,12 +646,34 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
561
646
  };
562
647
  const handleDelete = async ()=>{
563
648
  try {
649
+ mutation.assertWritable();
564
650
  const result = await deleteDocument({
565
651
  data: {
652
+ expectedRevision: mutation.expectedRevision(),
566
653
  collection: path,
567
654
  id: String(initialData.id)
568
655
  }
569
656
  });
657
+ if (result.scheduledPublicationsNeedReconfirmation) toastManager.add({
658
+ title: t('documentConcurrency.schedulesTitle'),
659
+ description: /*#__PURE__*/ jsxs(Fragment, {
660
+ children: [
661
+ /*#__PURE__*/ jsx("p", {
662
+ children: t('documentConcurrency.schedules')
663
+ }),
664
+ /*#__PURE__*/ jsx("a", {
665
+ href: getAdminRoutePath('scheduled-publications'),
666
+ children: t('documentConcurrency.reviewSchedules')
667
+ })
668
+ ]
669
+ }),
670
+ data: {
671
+ intent: 'warning',
672
+ iconType: 'warning',
673
+ icon: true,
674
+ close: true
675
+ }
676
+ });
570
677
  const hasSideEffectFailures = hasDeleteSideEffectFailures(result);
571
678
  const description = hasSideEffectFailures ? t("collections.edit.deletedWithWarningsDescription", {
572
679
  label: singular
@@ -598,9 +705,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
598
705
  search: returnSearch
599
706
  });
600
707
  } catch (err) {
708
+ const handled = mutation.report(err);
709
+ if ('blocked' === handled) throw err;
710
+ if ('committed' === handled) {
711
+ notifyCommittedHookFailure();
712
+ reloadDocument();
713
+ return;
714
+ }
601
715
  console.error('Delete error:', err);
602
716
  const description = t("collections.edit.deleteFailedDescription", {
603
- message: err.message
717
+ message: t('documentConcurrency.failed')
604
718
  });
605
719
  toastManager.add({
606
720
  title: t('collections.edit.deleteTitle', {
@@ -622,11 +736,13 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
622
736
  };
623
737
  const handleSubmit = async ({ data: _data, patches, contentDirty, pathDirty, systemPath, availableLocalesDirty, systemAvailableLocales })=>{
624
738
  try {
625
- let hookFailed = false;
626
- if (pathDirty || availableLocalesDirty) await updateCollectionDocumentSystemFields({
739
+ mutation.assertWritable();
740
+ const result = await updateCollectionDocumentWithPatches({
627
741
  data: {
628
742
  collection: path,
629
743
  id: String(initialData.id),
744
+ expectedRevision: mutation.expectedRevision(),
745
+ patches: contentDirty ? patches : [],
630
746
  locale,
631
747
  ...pathDirty ? {
632
748
  path: systemPath ?? null
@@ -636,19 +752,9 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
636
752
  } : {}
637
753
  }
638
754
  });
639
- if (contentDirty) {
640
- const result = await updateCollectionDocumentWithPatches({
641
- data: {
642
- collection: path,
643
- id: String(initialData.id),
644
- patches,
645
- versionId: initialData.versionId,
646
- locale
647
- }
648
- });
649
- hookFailed = hasCommittedDocumentHookFailure(result);
650
- }
651
- if (contentDirty && scheduledPublication?.state === 'armed') notifyScheduleSuspended();
755
+ mutation.adopt(result);
756
+ const hookFailed = hasCommittedDocumentHookFailure(result);
757
+ if ((contentDirty || pathDirty || availableLocalesDirty) && scheduledPublication?.state === 'armed') notifyScheduleSuspended();
652
758
  const description = hookFailed ? t("collections.save.hookFailedDescription") : t("collections.edit.updatedDescription", {
653
759
  label: singularLower
654
760
  });
@@ -680,6 +786,13 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
680
786
  ignoreBlocker: true
681
787
  });
682
788
  } catch (err) {
789
+ const handled = mutation.report(err);
790
+ if ('blocked' === handled) throw err;
791
+ if ('committed' === handled) {
792
+ notifyCommittedHookFailure();
793
+ reloadDocument();
794
+ return;
795
+ }
683
796
  console.error('Network error:', err);
684
797
  const description = t("collections.edit.updateFailedDescription", {
685
798
  label: singularLower
@@ -706,6 +819,13 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
706
819
  return /*#__PURE__*/ jsx(Section, {
707
820
  children: /*#__PURE__*/ jsx(Container, {
708
821
  children: /*#__PURE__*/ jsx(FormRenderer, {
822
+ mutationIssue: mutation.issue,
823
+ mutationsBlocked: mutation.blocked,
824
+ observedRevision: mutation.revision,
825
+ onMutationError: mutation.report,
826
+ onTreeMutationCommitted: mutation.adopt,
827
+ scheduledPublicationsNeedReconfirmation: mutation.scheduleNotice,
828
+ scheduledPublicationsHref: getAdminRoutePath('scheduled-publications'),
709
829
  mode: "edit",
710
830
  fields: fields,
711
831
  onSubmit: handleSubmit,
@@ -91,7 +91,7 @@ const HistoryView = ({ collectionDefinition, adminConfig, data, auditLog, workfl
91
91
  ];
92
92
  }),
93
93
  auditColSpan: columns.length + (hasRestoreColumn ? 1 : 0),
94
- renderCells: ({ document, versionId, versionNumber, currentVersionId, restoreStatusLabel, openCompare, openRestore })=>columns.flatMap((column)=>{
94
+ renderCells: ({ document, versionId, versionNumber, currentVersionId, restoreStatusLabel, openCompare, mutationsBlocked, openRestore })=>columns.flatMap((column)=>{
95
95
  const value = getColumnValue(document, String(column.fieldName));
96
96
  const dataCell = /*#__PURE__*/ jsx(Table.Cell, {
97
97
  className: clsx({
@@ -126,6 +126,7 @@ const HistoryView = ({ collectionDefinition, adminConfig, data, auditLog, workfl
126
126
  variant: "outlined",
127
127
  size: "xs",
128
128
  intent: "noeffect",
129
+ disabled: mutationsBlocked,
129
130
  onClick: openRestore,
130
131
  className: clsx('byline-coll-history-restore-button', history_module.restoreButton),
131
132
  title: t('collections.history.restoreButtonTitle', {
@@ -209,8 +210,9 @@ const HistoryView = ({ collectionDefinition, adminConfig, data, auditLog, workfl
209
210
  rowPresentation: rowPresentation,
210
211
  loadHistoricalVersion: getCollectionDocumentVersion,
211
212
  onPageSizeChange: handlePageSizeChange,
212
- restoreVersion: (versionId)=>restoreDocumentVersion({
213
+ restoreVersion: (versionId, expectedRevision)=>restoreDocumentVersion({
213
214
  data: {
215
+ expectedRevision,
214
216
  collection,
215
217
  id,
216
218
  versionId
@@ -1,4 +1,6 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { DocumentMutationNotice } from "../document-mutation-notice.js";
3
+ import { useDocumentMutationState } from "../document-mutation-state.js";
2
4
  import { useEffect, useMemo, useState } from "react";
3
5
  import { useRouter, useRouterState } from "@tanstack/react-router";
4
6
  import { StatusBadge, renderFormatted } from "@byline/admin/react";
@@ -77,6 +79,7 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
77
79
  const navigate = useNavigate();
78
80
  const router = useRouter();
79
81
  const toastManager = useToastManager();
82
+ const mutation = useDocumentMutationState(void 0);
80
83
  const { t } = useTranslation('byline-admin');
81
84
  const location = useRouterState({
82
85
  select: (s)=>s.location
@@ -84,14 +87,15 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
84
87
  const [localDocs, setLocalDocs] = useState(data.docs);
85
88
  const [isReordering, setIsReordering] = useState(false);
86
89
  useEffect(()=>{
87
- if (!isReordering) setLocalDocs(data.docs);
90
+ if (!isReordering && !mutation.blocked) setLocalDocs(data.docs);
88
91
  }, [
89
92
  data.docs,
90
- isReordering
93
+ isReordering,
94
+ mutation.blocked
91
95
  ]);
92
96
  const searchParams = location.search;
93
97
  const isCanonicalView = !searchParams.order && !searchParams.desc && !searchParams.query && !searchParams.status;
94
- const dragEnabled = orderable && isCanonicalView && !!onReorder;
98
+ const dragEnabled = orderable && isCanonicalView && !!onReorder && !mutation.blocked;
95
99
  const activeOrder = searchParams.order ?? data?.meta.order;
96
100
  const activeDesc = null != searchParams.order ? searchParams.desc : data?.meta.desc;
97
101
  const sensors = useSensors(useSensor(PointerSensor, {
@@ -103,7 +107,7 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
103
107
  }));
104
108
  const handleDragEnd = async (event)=>{
105
109
  const { active, over } = event;
106
- if (!over || active.id === over.id || !onReorder) return;
110
+ if (mutation.blocked || isReordering || !over || active.id === over.id || !onReorder) return;
107
111
  const oldIndex = localDocs.findIndex((d)=>d.id === active.id);
108
112
  const newIndex = localDocs.findIndex((d)=>d.id === over.id);
109
113
  if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) return;
@@ -119,14 +123,30 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
119
123
  const before = next[newIndex - 1]?.id ?? null;
120
124
  const after = next[newIndex + 1]?.id ?? null;
121
125
  try {
122
- await onReorder({
126
+ mutation.assertWritable();
127
+ const receipt = await onReorder({
123
128
  documentId: String(active.id),
124
129
  beforeDocumentId: before,
125
130
  afterDocumentId: after
126
131
  });
127
- await router.invalidate();
132
+ mutation.reportStructure(receipt);
133
+ try {
134
+ await router.invalidate();
135
+ } catch {
136
+ toastManager.add({
137
+ title: t('collections.list.treeRefreshFailedToast'),
138
+ description: t("collections.list.treeRefreshFailedDescription"),
139
+ data: {
140
+ intent: 'warning',
141
+ iconType: 'warning',
142
+ icon: true,
143
+ close: true
144
+ }
145
+ });
146
+ }
128
147
  } catch (_err) {
129
148
  setLocalDocs(previousDocs);
149
+ if (mutation.report(_err)) return;
130
150
  toastManager.add({
131
151
  title: t('collections.list.reorderFailedToast'),
132
152
  description: t("collections.list.reorderFailedDescription"),
@@ -241,6 +261,10 @@ const ListView = ({ data, columns, workflowStatuses, useAsTitle, orderable = fal
241
261
  return /*#__PURE__*/ jsx(Section, {
242
262
  children: /*#__PURE__*/ jsxs(Container, {
243
263
  children: [
264
+ /*#__PURE__*/ jsx(DocumentMutationNotice, {
265
+ issue: mutation.issue,
266
+ scheduleNotice: mutation.scheduleNotice
267
+ }),
244
268
  /*#__PURE__*/ jsxs("div", {
245
269
  className: clsx('byline-coll-list-head', list_module.head),
246
270
  children: [
@@ -1,4 +1,6 @@
1
1
  interface RestoreVersionModalProps {
2
+ disabled?: boolean;
3
+ onMutationError?: (error: unknown) => unknown;
2
4
  versionLabel: string;
3
5
  versionNumber: number;
4
6
  restoreStatusLabel: string;
@@ -6,5 +8,5 @@ interface RestoreVersionModalProps {
6
8
  restoreVersion: () => Promise<unknown>;
7
9
  onRestoreComplete: () => void | Promise<void>;
8
10
  }
9
- export declare function RestoreVersionModal({ versionLabel, versionNumber, restoreStatusLabel, onClose, restoreVersion, onRestoreComplete, }: RestoreVersionModalProps): import("react").JSX.Element;
11
+ export declare function RestoreVersionModal({ disabled, onMutationError, versionLabel, versionNumber, restoreStatusLabel, onClose, restoreVersion, onRestoreComplete, }: RestoreVersionModalProps): import("react").JSX.Element;
10
12
  export {};
@@ -6,13 +6,13 @@ import { Alert, Button, LoaderEllipsis, Modal, useToastManager } from "@byline/u
6
6
  import clsx from "clsx";
7
7
  import { hasCommittedDocumentHookFailure } from "../../server-fns/collections/save-outcome.js";
8
8
  import restore_version_modal_module from "./restore-version-modal.module.js";
9
- function RestoreVersionModal({ versionLabel, versionNumber, restoreStatusLabel, onClose, restoreVersion, onRestoreComplete }) {
9
+ function RestoreVersionModal({ disabled = false, onMutationError, versionLabel, versionNumber, restoreStatusLabel, onClose, restoreVersion, onRestoreComplete }) {
10
10
  const { t } = useTranslation('byline-admin');
11
11
  const toastManager = useToastManager();
12
12
  const [error, setError] = useState(null);
13
13
  const [pending, setPending] = useState(false);
14
14
  async function handleRestore() {
15
- if (pending) return;
15
+ if (pending || disabled) return;
16
16
  setPending(true);
17
17
  setError(null);
18
18
  try {
@@ -30,6 +30,11 @@ function RestoreVersionModal({ versionLabel, versionNumber, restoreStatusLabel,
30
30
  onClose();
31
31
  await onRestoreComplete();
32
32
  } catch (err) {
33
+ if (onMutationError?.(err)) {
34
+ setPending(false);
35
+ onClose();
36
+ return;
37
+ }
33
38
  const code = getErrorCode(err);
34
39
  if ('ERR_INVALID_TRANSITION' === code) setError(t('collections.restore.errors.alreadyCurrent'));
35
40
  else if ('ERR_NOT_FOUND' === code) setError(t('collections.restore.errors.notFound'));
@@ -97,7 +102,7 @@ function RestoreVersionModal({ versionLabel, versionNumber, restoreStatusLabel,
97
102
  intent: "noeffect",
98
103
  size: "sm",
99
104
  onClick: onClose,
100
- disabled: pending,
105
+ disabled: pending || disabled,
101
106
  className: clsx('byline-coll-restore-button', restore_version_modal_module.button),
102
107
  children: t('common.actions.cancel')
103
108
  }),
@@ -108,7 +113,7 @@ function RestoreVersionModal({ versionLabel, versionNumber, restoreStatusLabel,
108
113
  style: {
109
114
  minWidth: '80px'
110
115
  },
111
- disabled: pending,
116
+ disabled: pending || disabled,
112
117
  className: clsx('byline-coll-restore-button', restore_version_modal_module.button),
113
118
  children: true === pending ? /*#__PURE__*/ jsx(LoaderEllipsis, {
114
119
  size: 42