@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.
Files changed (148) hide show
  1. package/dist/admin-shell/collections/create.js +22 -5
  2. package/dist/admin-shell/collections/edit.d.ts +1 -1
  3. package/dist/admin-shell/collections/edit.js +222 -34
  4. package/dist/admin-shell/collections/history.js +4 -2
  5. package/dist/admin-shell/collections/list.js +30 -6
  6. package/dist/admin-shell/collections/restore-version-modal.d.ts +3 -1
  7. package/dist/admin-shell/collections/restore-version-modal.js +23 -6
  8. package/dist/admin-shell/collections/save-navigation.test.d.ts +8 -0
  9. package/dist/admin-shell/collections/tree-list.js +16 -6
  10. package/dist/admin-shell/collections/version-history.d.ts +2 -1
  11. package/dist/admin-shell/collections/version-history.js +11 -2
  12. package/dist/admin-shell/document-mutation-notice.d.ts +6 -0
  13. package/dist/admin-shell/document-mutation-notice.js +53 -0
  14. package/dist/admin-shell/document-mutation-state.d.ts +22 -0
  15. package/dist/admin-shell/document-mutation-state.js +71 -0
  16. package/dist/admin-shell/document-mutation-state.test.d.ts +1 -0
  17. package/dist/admin-shell/scheduled-publications/list.js +15 -5
  18. package/dist/admin-shell/singletons/history.js +4 -2
  19. package/dist/admin-shell/singletons/view.d.ts +1 -1
  20. package/dist/admin-shell/singletons/view.js +108 -23
  21. package/dist/integrations/byline-field-services.js +2 -9
  22. package/dist/routes/create-collection-edit-route.js +1 -1
  23. package/dist/routes/create-collection-list-route.d.ts +0 -7
  24. package/dist/routes/create-collection-list-route.js +7 -9
  25. package/dist/routes/create-singleton-route.d.ts +2 -0
  26. package/dist/routes/create-singleton-route.js +4 -3
  27. package/dist/server-fns/collections/copy-to-locale.d.ts +4 -8
  28. package/dist/server-fns/collections/copy-to-locale.js +17 -8
  29. package/dist/server-fns/collections/create.d.ts +2 -8
  30. package/dist/server-fns/collections/create.js +20 -10
  31. package/dist/server-fns/collections/delete-locale.d.ts +4 -8
  32. package/dist/server-fns/collections/delete-locale.js +15 -6
  33. package/dist/server-fns/collections/delete-outcome.d.ts +7 -3
  34. package/dist/server-fns/collections/delete-outcome.js +4 -0
  35. package/dist/server-fns/collections/delete.d.ts +2 -7
  36. package/dist/server-fns/collections/delete.js +5 -3
  37. package/dist/server-fns/collections/duplicate.d.ts +4 -8
  38. package/dist/server-fns/collections/duplicate.js +14 -5
  39. package/dist/server-fns/collections/get.d.ts +1 -7
  40. package/dist/server-fns/collections/get.js +5 -4
  41. package/dist/server-fns/collections/index.d.ts +1 -0
  42. package/dist/server-fns/collections/index.js +1 -0
  43. package/dist/server-fns/collections/list.d.ts +0 -7
  44. package/dist/server-fns/collections/list.js +11 -3
  45. package/dist/server-fns/collections/reorder.d.ts +7 -8
  46. package/dist/server-fns/collections/reorder.js +20 -76
  47. package/dist/server-fns/collections/restore-version.d.ts +4 -8
  48. package/dist/server-fns/collections/restore-version.js +22 -12
  49. package/dist/server-fns/collections/save-outcome.d.ts +24 -0
  50. package/dist/server-fns/collections/save-outcome.js +19 -0
  51. package/dist/server-fns/collections/scheduled-publication.d.ts +49 -9
  52. package/dist/server-fns/collections/scheduled-publication.js +31 -21
  53. package/dist/server-fns/collections/status.d.ts +8 -7
  54. package/dist/server-fns/collections/status.js +13 -5
  55. package/dist/server-fns/collections/tree-mutation.d.ts +7 -6
  56. package/dist/server-fns/collections/tree-mutation.js +17 -5
  57. package/dist/server-fns/collections/tree.d.ts +14 -8
  58. package/dist/server-fns/collections/tree.js +21 -17
  59. package/dist/server-fns/collections/update.d.ts +14 -11
  60. package/dist/server-fns/collections/update.js +48 -24
  61. package/dist/server-fns/collections/upload.d.ts +1 -0
  62. package/dist/server-fns/collections/upload.js +3 -2
  63. package/dist/server-fns/document-mutation-errors.d.ts +5 -0
  64. package/dist/server-fns/document-mutation-errors.js +50 -0
  65. package/dist/server-fns/editable-revision.d.ts +9 -0
  66. package/dist/server-fns/editable-revision.js +11 -0
  67. package/dist/server-fns/singleton-document-read.d.ts +1 -1
  68. package/dist/server-fns/singleton-document-read.js +7 -3
  69. package/dist/server-fns/singletons/cancel-scheduled-publish.d.ts +6 -8
  70. package/dist/server-fns/singletons/cancel-scheduled-publish.js +4 -1
  71. package/dist/server-fns/singletons/change-status.d.ts +2 -7
  72. package/dist/server-fns/singletons/change-status.js +4 -1
  73. package/dist/server-fns/singletons/confirm-scheduled-publish.d.ts +5 -8
  74. package/dist/server-fns/singletons/confirm-scheduled-publish.js +4 -2
  75. package/dist/server-fns/singletons/copy-to-locale.d.ts +2 -7
  76. package/dist/server-fns/singletons/copy-to-locale.js +16 -5
  77. package/dist/server-fns/singletons/get.d.ts +4 -4
  78. package/dist/server-fns/singletons/restore-version.d.ts +2 -7
  79. package/dist/server-fns/singletons/restore-version.js +13 -1
  80. package/dist/server-fns/singletons/schedule-publish.d.ts +5 -8
  81. package/dist/server-fns/singletons/schedule-publish.js +4 -2
  82. package/dist/server-fns/singletons/unpublish.d.ts +2 -7
  83. package/dist/server-fns/singletons/unpublish.js +4 -1
  84. package/dist/server-fns/singletons/update.d.ts +4 -11
  85. package/dist/server-fns/singletons/update.js +16 -8
  86. package/dist/server-fns/structural-receipt.d.ts +4 -0
  87. package/dist/server-fns/structural-receipt.js +31 -0
  88. package/package.json +11 -11
  89. package/src/admin-shell/collections/create.tsx +22 -3
  90. package/src/admin-shell/collections/edit.tsx +233 -61
  91. package/src/admin-shell/collections/history.tsx +11 -2
  92. package/src/admin-shell/collections/list.tsx +22 -6
  93. package/src/admin-shell/collections/restore-version-modal.tsx +23 -5
  94. package/src/admin-shell/collections/save-navigation.test.tsx +532 -0
  95. package/src/admin-shell/collections/tree-list.tsx +16 -6
  96. package/src/admin-shell/collections/version-history.tsx +13 -3
  97. package/src/admin-shell/document-mutation-notice.tsx +45 -0
  98. package/src/admin-shell/document-mutation-state.test.tsx +146 -0
  99. package/src/admin-shell/document-mutation-state.ts +95 -0
  100. package/src/admin-shell/scheduled-publications/list.tsx +16 -6
  101. package/src/admin-shell/singletons/history.test.tsx +46 -1
  102. package/src/admin-shell/singletons/history.tsx +10 -2
  103. package/src/admin-shell/singletons/view.test.tsx +86 -18
  104. package/src/admin-shell/singletons/view.tsx +145 -45
  105. package/src/integrations/byline-field-services.ts +2 -3
  106. package/src/routes/create-collection-edit-route.tsx +1 -0
  107. package/src/routes/create-collection-list-route.tsx +13 -2
  108. package/src/routes/create-singleton-route.test.node.ts +10 -2
  109. package/src/routes/create-singleton-route.tsx +6 -4
  110. package/src/server-fns/collections/copy-to-locale.ts +48 -29
  111. package/src/server-fns/collections/create.ts +56 -44
  112. package/src/server-fns/collections/delete-locale.ts +48 -25
  113. package/src/server-fns/collections/delete-outcome.test.node.ts +16 -0
  114. package/src/server-fns/collections/delete-outcome.ts +27 -16
  115. package/src/server-fns/collections/delete.ts +44 -38
  116. package/src/server-fns/collections/duplicate.ts +45 -25
  117. package/src/server-fns/collections/get.ts +8 -4
  118. package/src/server-fns/collections/index.ts +1 -0
  119. package/src/server-fns/collections/list.ts +8 -3
  120. package/src/server-fns/collections/reorder.ts +32 -124
  121. package/src/server-fns/collections/restore-version.ts +51 -36
  122. package/src/server-fns/collections/revision-input.test.node.ts +108 -0
  123. package/src/server-fns/collections/save-outcome.test.node.ts +80 -0
  124. package/src/server-fns/collections/save-outcome.ts +57 -0
  125. package/src/server-fns/collections/scheduled-publication.ts +138 -111
  126. package/src/server-fns/collections/status.ts +86 -64
  127. package/src/server-fns/collections/tree-mutation.test.node.ts +87 -29
  128. package/src/server-fns/collections/tree-mutation.ts +28 -11
  129. package/src/server-fns/collections/tree.ts +150 -122
  130. package/src/server-fns/collections/update.ts +110 -71
  131. package/src/server-fns/collections/upload-singleton.test.node.ts +3 -7
  132. package/src/server-fns/collections/upload.ts +81 -77
  133. package/src/server-fns/document-mutation-errors.ts +67 -0
  134. package/src/server-fns/document-mutation-transport.test.node.ts +211 -0
  135. package/src/server-fns/editable-revision.test.node.ts +23 -0
  136. package/src/server-fns/editable-revision.ts +18 -0
  137. package/src/server-fns/singleton-document-read.ts +7 -3
  138. package/src/server-fns/singletons/cancel-scheduled-publish.ts +10 -3
  139. package/src/server-fns/singletons/change-status.ts +10 -3
  140. package/src/server-fns/singletons/confirm-scheduled-publish.ts +12 -6
  141. package/src/server-fns/singletons/copy-to-locale.ts +20 -8
  142. package/src/server-fns/singletons/get.ts +3 -3
  143. package/src/server-fns/singletons/restore-version.ts +17 -3
  144. package/src/server-fns/singletons/schedule-publish.ts +18 -7
  145. package/src/server-fns/singletons/singletons.test.node.ts +96 -24
  146. package/src/server-fns/singletons/unpublish.ts +10 -3
  147. package/src/server-fns/singletons/update.ts +21 -9
  148. package/src/server-fns/structural-receipt.ts +41 -0
@@ -5,7 +5,7 @@ import { useTranslation } from "@byline/i18n/react";
5
5
  import { Container, Section, useToastManager } from "@byline/ui/react";
6
6
  import { getAdminRoutePath } from "../../routes/admin-path.js";
7
7
  import { decodeListReturnState } from "../../routes/list-return-state.js";
8
- import { createCollectionDocument } from "../../server-fns/collections/index.js";
8
+ import { createCollectionDocument, hasCommittedDocumentHookFailure } from "../../server-fns/collections/index.js";
9
9
  import { useNavigate } from "../chrome/loose-router.js";
10
10
  import { useTanStackNavigationGuard } from "./tanstack-navigation-guard.js";
11
11
  const CreateView = ({ collectionDefinition, adminConfig, initialData, from })=>{
@@ -31,6 +31,17 @@ const CreateView = ({ collectionDefinition, adminConfig, initialData, from })=>{
31
31
  } : {}
32
32
  }
33
33
  });
34
+ const hookFailed = hasCommittedDocumentHookFailure(result);
35
+ if (hookFailed) toastManager.add({
36
+ title: t('collections.save.hookFailedToast'),
37
+ description: t("collections.save.hookFailedDescription"),
38
+ data: {
39
+ intent: 'warning',
40
+ iconType: 'warning',
41
+ icon: true,
42
+ close: true
43
+ }
44
+ });
34
45
  result?.documentId ? navigate({
35
46
  to: getAdminRoutePath('collections', '$collection', '$id'),
36
47
  params: {
@@ -38,9 +49,12 @@ const CreateView = ({ collectionDefinition, adminConfig, initialData, from })=>{
38
49
  id: result.documentId
39
50
  },
40
51
  search: {
41
- action: 'created',
52
+ ...hookFailed ? {} : {
53
+ action: 'created'
54
+ },
42
55
  from
43
- }
56
+ },
57
+ ignoreBlocker: true
44
58
  }) : navigate({
45
59
  to: getAdminRoutePath('collections', '$collection'),
46
60
  params: {
@@ -48,8 +62,11 @@ const CreateView = ({ collectionDefinition, adminConfig, initialData, from })=>{
48
62
  },
49
63
  search: {
50
64
  ...decodeListReturnState(from),
51
- action: 'created'
52
- }
65
+ ...hookFailed ? {} : {
66
+ action: 'created'
67
+ }
68
+ },
69
+ ignoreBlocker: true
53
70
  });
54
71
  } catch (err) {
55
72
  console.error(err);
@@ -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, 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({
@@ -43,6 +46,23 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
43
46
  }
44
47
  });
45
48
  };
49
+ const notifyCommittedHookFailure = ()=>{
50
+ const description = t("collections.save.hookFailedDescription");
51
+ toastManager.add({
52
+ title: t('collections.save.hookFailedToast'),
53
+ description,
54
+ data: {
55
+ intent: 'warning',
56
+ iconType: 'warning',
57
+ icon: true,
58
+ close: true
59
+ }
60
+ });
61
+ setEditState({
62
+ status: 'warning',
63
+ message: description
64
+ });
65
+ };
46
66
  const handleLocaleChange = (newLocale)=>{
47
67
  navigate({
48
68
  to: getAdminRoutePath('collections', '$collection', '$id'),
@@ -58,13 +78,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
58
78
  };
59
79
  const handleStatusChange = async (status)=>{
60
80
  try {
61
- await updateDocumentStatus({
81
+ mutation.assertWritable();
82
+ const result = await updateDocumentStatus({
62
83
  data: {
84
+ expectedRevision: mutation.expectedRevision(),
63
85
  collection: path,
64
86
  id: String(initialData.id),
65
87
  status
66
88
  }
67
89
  });
90
+ mutation.adopt(result);
68
91
  const description = t("collections.edit.statusChangedDescription", {
69
92
  status
70
93
  });
@@ -95,9 +118,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
95
118
  })
96
119
  });
97
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
+ }
98
128
  console.error('Status change error:', err);
99
129
  const description = t("collections.edit.statusChangeFailedDescription", {
100
- message: err.message
130
+ message: t('documentConcurrency.failed')
101
131
  });
102
132
  toastManager.add({
103
133
  title: t('collections.edit.statusUpdateTitle', {
@@ -131,14 +161,17 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
131
161
  };
132
162
  const handleSchedulePublication = async ({ publishAt })=>{
133
163
  try {
134
- await scheduleCollectionDocumentPublish({
164
+ mutation.assertWritable();
165
+ const result = await scheduleCollectionDocumentPublish({
135
166
  data: {
167
+ expectedRevision: mutation.expectedRevision(),
136
168
  collection: path,
137
169
  id: String(initialData.id),
138
170
  publishAt,
139
171
  expectedVersionId: String(initialData.versionId)
140
172
  }
141
173
  });
174
+ mutation.adopt(result);
142
175
  toastManager.add({
143
176
  title: t('scheduledPublication.toast.scheduledTitle'),
144
177
  description: t("scheduledPublication.toast.scheduledDescription"),
@@ -151,9 +184,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
151
184
  });
152
185
  reloadDocument();
153
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
+ }
154
194
  toastManager.add({
155
195
  title: t('scheduledPublication.toast.failedTitle'),
156
- description: err.message,
196
+ description: t('documentConcurrency.failed'),
157
197
  data: {
158
198
  intent: 'danger',
159
199
  iconType: 'danger',
@@ -166,13 +206,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
166
206
  };
167
207
  const handleConfirmScheduledPublication = async ()=>{
168
208
  try {
169
- await confirmCollectionDocumentScheduledPublish({
209
+ mutation.assertWritable();
210
+ const result = await confirmCollectionDocumentScheduledPublish({
170
211
  data: {
212
+ expectedRevision: mutation.expectedRevision(),
171
213
  collection: path,
172
214
  id: String(initialData.id),
173
215
  expectedVersionId: String(initialData.versionId)
174
216
  }
175
217
  });
218
+ mutation.adopt(result);
176
219
  toastManager.add({
177
220
  title: t('scheduledPublication.toast.confirmedTitle'),
178
221
  description: t("scheduledPublication.toast.confirmedDescription"),
@@ -185,9 +228,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
185
228
  });
186
229
  reloadDocument();
187
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
+ }
188
238
  toastManager.add({
189
239
  title: t('scheduledPublication.toast.failedTitle'),
190
- description: err.message,
240
+ description: t('documentConcurrency.failed'),
191
241
  data: {
192
242
  intent: 'danger',
193
243
  iconType: 'danger',
@@ -200,12 +250,18 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
200
250
  };
201
251
  const handleCancelScheduledPublication = async ()=>{
202
252
  try {
253
+ mutation.assertWritable();
203
254
  const result = await cancelCollectionDocumentScheduledPublish({
204
255
  data: {
256
+ expectedRevision: mutation.expectedRevision(),
205
257
  collection: path,
206
258
  id: String(initialData.id)
207
259
  }
208
260
  });
261
+ mutation.adopt({
262
+ ...result,
263
+ documentId: String(initialData.id)
264
+ });
209
265
  const cancelled = 'cancelled' === result.status;
210
266
  toastManager.add({
211
267
  title: cancelled ? t('scheduledPublication.toast.cancelledTitle') : t('scheduledPublication.toast.cancelLostTitle'),
@@ -219,9 +275,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
219
275
  });
220
276
  reloadDocument();
221
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
+ }
222
285
  toastManager.add({
223
286
  title: t('scheduledPublication.toast.failedTitle'),
224
- description: err.message,
287
+ description: t('documentConcurrency.failed'),
225
288
  data: {
226
289
  intent: 'danger',
227
290
  iconType: 'danger',
@@ -236,12 +299,15 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
236
299
  const restoreWarnings = initialData?._restoreWarnings;
237
300
  const handleUnpublish = async ()=>{
238
301
  try {
239
- await unpublishDocument({
302
+ mutation.assertWritable();
303
+ const result = await unpublishDocument({
240
304
  data: {
305
+ expectedRevision: mutation.expectedRevision(),
241
306
  collection: path,
242
307
  id: String(initialData.id)
243
308
  }
244
309
  });
310
+ mutation.adopt(result);
245
311
  const description = t("collections.edit.unpublishedDescription");
246
312
  toastManager.add({
247
313
  title: t('collections.edit.unpublishTitle', {
@@ -270,9 +336,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
270
336
  })
271
337
  });
272
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
+ }
273
346
  console.error('Unpublish error:', err);
274
347
  const description = t("collections.edit.unpublishFailedDescription", {
275
- message: err.message
348
+ message: t('documentConcurrency.failed')
276
349
  });
277
350
  toastManager.add({
278
351
  title: t('collections.edit.unpublishTitle', {
@@ -294,12 +367,28 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
294
367
  };
295
368
  const handleDuplicate = async ()=>{
296
369
  try {
370
+ mutation.assertWritable();
297
371
  const result = await duplicateCollectionDocument({
298
372
  data: {
373
+ expectedRevision: mutation.expectedRevision(),
299
374
  collection: path,
300
375
  id: String(initialData.id)
301
376
  }
302
377
  });
378
+ if (hasCommittedDocumentHookFailure(result)) {
379
+ notifyCommittedHookFailure();
380
+ navigate({
381
+ to: getAdminRoutePath('collections', '$collection', '$id'),
382
+ params: {
383
+ collection: path,
384
+ id: result.documentId
385
+ },
386
+ search: (prev)=>({
387
+ ...prev
388
+ })
389
+ });
390
+ return;
391
+ }
303
392
  const description = result.pathRetried ? t("collections.edit.duplicatedAutoPathDescription", {
304
393
  path: result.newPath
305
394
  }) : t("collections.edit.duplicatedPathDescription", {
@@ -334,9 +423,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
334
423
  })
335
424
  });
336
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
+ }
337
433
  console.error('Duplicate error:', err);
338
434
  const description = t("collections.edit.duplicateFailedDescription", {
339
- message: err.message
435
+ message: t('documentConcurrency.failed')
340
436
  });
341
437
  toastManager.add({
342
438
  title: t('collections.edit.duplicateTitle', {
@@ -358,8 +454,10 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
358
454
  };
359
455
  const handleCopyToLocale = async ({ targetLocale, overwrite })=>{
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,23 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
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: {
475
+ collection: path,
476
+ id: String(initialData.id)
477
+ },
478
+ search: (prev)=>({
479
+ ...prev,
480
+ locale: targetLocale
481
+ })
482
+ });
483
+ return;
484
+ }
370
485
  const sourceLabel = contentLocales.find((l)=>l.code === result.sourceLocale)?.label ?? result.sourceLocale;
371
486
  const targetLabel = contentLocales.find((l)=>l.code === result.targetLocale)?.label ?? result.targetLocale;
372
487
  const description = result.fieldsUpdated > 0 ? t("collections.edit.copiedFieldsDescription", {
@@ -409,9 +524,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
409
524
  })
410
525
  });
411
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
+ }
412
534
  console.error('Copy to locale error:', err);
413
535
  const description = t("collections.edit.copyFailedDescription", {
414
- message: err.message
536
+ message: t('documentConcurrency.failed')
415
537
  });
416
538
  toastManager.add({
417
539
  title: t('collections.edit.copyToLocaleTitle', {
@@ -433,13 +555,32 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
433
555
  };
434
556
  const handleDeleteLocale = async ({ targetLocale })=>{
435
557
  try {
558
+ mutation.assertWritable();
436
559
  const result = await deleteDocumentLocale({
437
560
  data: {
561
+ expectedRevision: mutation.expectedRevision(),
438
562
  collection: path,
439
563
  id: String(initialData.id),
440
564
  locale: targetLocale
441
565
  }
442
566
  });
567
+ mutation.adopt(result);
568
+ if (hasCommittedDocumentHookFailure(result)) {
569
+ notifyCommittedHookFailure();
570
+ notifyScheduleSuspended();
571
+ navigate({
572
+ to: getAdminRoutePath('collections', '$collection', '$id'),
573
+ params: {
574
+ collection: path,
575
+ id: String(initialData.id)
576
+ },
577
+ search: (prev)=>({
578
+ ...prev,
579
+ locale: defaultContentLocale
580
+ })
581
+ });
582
+ return;
583
+ }
443
584
  const targetLabel = contentLocales.find((l)=>l.code === result.locale)?.label ?? result.locale;
444
585
  const description = t("collections.edit.deletedLocaleDescription", {
445
586
  label: singular,
@@ -474,9 +615,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
474
615
  })
475
616
  });
476
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
+ }
477
625
  console.error('Delete locale error:', err);
478
626
  const description = t("collections.edit.deleteLocaleFailedDescription", {
479
- message: err.message
627
+ message: t('documentConcurrency.failed')
480
628
  });
481
629
  toastManager.add({
482
630
  title: t('collections.edit.deleteLocaleTitle', {
@@ -498,12 +646,34 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
498
646
  };
499
647
  const handleDelete = async ()=>{
500
648
  try {
649
+ mutation.assertWritable();
501
650
  const result = await deleteDocument({
502
651
  data: {
652
+ expectedRevision: mutation.expectedRevision(),
503
653
  collection: path,
504
654
  id: String(initialData.id)
505
655
  }
506
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
+ });
507
677
  const hasSideEffectFailures = hasDeleteSideEffectFailures(result);
508
678
  const description = hasSideEffectFailures ? t("collections.edit.deletedWithWarningsDescription", {
509
679
  label: singular
@@ -523,7 +693,7 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
523
693
  }
524
694
  });
525
695
  setEditState({
526
- status: 'success',
696
+ status: hasSideEffectFailures ? 'warning' : 'success',
527
697
  message: description
528
698
  });
529
699
  clearListReturnState(path, String(initialData.id));
@@ -535,9 +705,16 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
535
705
  search: returnSearch
536
706
  });
537
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
+ }
538
715
  console.error('Delete error:', err);
539
716
  const description = t("collections.edit.deleteFailedDescription", {
540
- message: err.message
717
+ message: t('documentConcurrency.failed')
541
718
  });
542
719
  toastManager.add({
543
720
  title: t('collections.edit.deleteTitle', {
@@ -559,10 +736,13 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
559
736
  };
560
737
  const handleSubmit = async ({ data: _data, patches, contentDirty, pathDirty, systemPath, availableLocalesDirty, systemAvailableLocales })=>{
561
738
  try {
562
- if (pathDirty || availableLocalesDirty) await updateCollectionDocumentSystemFields({
739
+ mutation.assertWritable();
740
+ const result = await updateCollectionDocumentWithPatches({
563
741
  data: {
564
742
  collection: path,
565
743
  id: String(initialData.id),
744
+ expectedRevision: mutation.expectedRevision(),
745
+ patches: contentDirty ? patches : [],
566
746
  locale,
567
747
  ...pathDirty ? {
568
748
  path: systemPath ?? null
@@ -572,33 +752,26 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
572
752
  } : {}
573
753
  }
574
754
  });
575
- if (contentDirty) await updateCollectionDocumentWithPatches({
576
- data: {
577
- collection: path,
578
- id: String(initialData.id),
579
- patches,
580
- versionId: initialData.versionId,
581
- locale
582
- }
583
- });
584
- if (contentDirty && scheduledPublication?.state === 'armed') notifyScheduleSuspended();
585
- const description = t("collections.edit.updatedDescription", {
755
+ mutation.adopt(result);
756
+ const hookFailed = hasCommittedDocumentHookFailure(result);
757
+ if ((contentDirty || pathDirty || availableLocalesDirty) && scheduledPublication?.state === 'armed') notifyScheduleSuspended();
758
+ const description = hookFailed ? t("collections.save.hookFailedDescription") : t("collections.edit.updatedDescription", {
586
759
  label: singularLower
587
760
  });
588
761
  toastManager.add({
589
- title: t('collections.edit.updateTitle', {
762
+ title: hookFailed ? t('collections.save.hookFailedToast') : t('collections.edit.updateTitle', {
590
763
  label: singular
591
764
  }),
592
765
  description,
593
766
  data: {
594
- intent: 'success',
595
- iconType: 'success',
767
+ intent: hookFailed ? 'warning' : 'success',
768
+ iconType: hookFailed ? 'warning' : 'success',
596
769
  icon: true,
597
770
  close: true
598
771
  }
599
772
  });
600
773
  setEditState({
601
- status: 'success',
774
+ status: hookFailed ? 'warning' : 'success',
602
775
  message: description
603
776
  });
604
777
  navigate({
@@ -609,9 +782,17 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
609
782
  },
610
783
  search: (prev)=>({
611
784
  ...prev
612
- })
785
+ }),
786
+ ignoreBlocker: true
613
787
  });
614
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
+ }
615
796
  console.error('Network error:', err);
616
797
  const description = t("collections.edit.updateFailedDescription", {
617
798
  label: singularLower
@@ -638,6 +819,13 @@ const EditView = ({ collectionDefinition, adminConfig, initialData, locale, cont
638
819
  return /*#__PURE__*/ jsx(Section, {
639
820
  children: /*#__PURE__*/ jsx(Container, {
640
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'),
641
829
  mode: "edit",
642
830
  fields: fields,
643
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