@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
@@ -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 {};
@@ -2,22 +2,39 @@
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { useTranslation } from "@byline/i18n/react";
5
- import { Alert, Button, LoaderEllipsis, Modal } from "@byline/ui/react";
5
+ import { Alert, Button, LoaderEllipsis, Modal, useToastManager } from "@byline/ui/react";
6
6
  import clsx from "clsx";
7
+ import { hasCommittedDocumentHookFailure } from "../../server-fns/collections/save-outcome.js";
7
8
  import restore_version_modal_module from "./restore-version-modal.module.js";
8
- function RestoreVersionModal({ versionLabel, versionNumber, restoreStatusLabel, onClose, restoreVersion, onRestoreComplete }) {
9
+ function RestoreVersionModal({ disabled = false, onMutationError, versionLabel, versionNumber, restoreStatusLabel, onClose, restoreVersion, onRestoreComplete }) {
9
10
  const { t } = useTranslation('byline-admin');
11
+ const toastManager = useToastManager();
10
12
  const [error, setError] = useState(null);
11
13
  const [pending, setPending] = useState(false);
12
14
  async function handleRestore() {
13
- if (pending) return;
15
+ if (pending || disabled) return;
14
16
  setPending(true);
15
17
  setError(null);
16
18
  try {
17
- await restoreVersion();
19
+ const result = await restoreVersion();
20
+ if (hasCommittedDocumentHookFailure(result)) toastManager.add({
21
+ title: t('collections.save.hookFailedToast'),
22
+ description: t("collections.save.hookFailedDescription"),
23
+ data: {
24
+ intent: 'warning',
25
+ iconType: 'warning',
26
+ icon: true,
27
+ close: true
28
+ }
29
+ });
18
30
  onClose();
19
31
  await onRestoreComplete();
20
32
  } catch (err) {
33
+ if (onMutationError?.(err)) {
34
+ setPending(false);
35
+ onClose();
36
+ return;
37
+ }
21
38
  const code = getErrorCode(err);
22
39
  if ('ERR_INVALID_TRANSITION' === code) setError(t('collections.restore.errors.alreadyCurrent'));
23
40
  else if ('ERR_NOT_FOUND' === code) setError(t('collections.restore.errors.notFound'));
@@ -85,7 +102,7 @@ function RestoreVersionModal({ versionLabel, versionNumber, restoreStatusLabel,
85
102
  intent: "noeffect",
86
103
  size: "sm",
87
104
  onClick: onClose,
88
- disabled: pending,
105
+ disabled: pending || disabled,
89
106
  className: clsx('byline-coll-restore-button', restore_version_modal_module.button),
90
107
  children: t('common.actions.cancel')
91
108
  }),
@@ -96,7 +113,7 @@ function RestoreVersionModal({ versionLabel, versionNumber, restoreStatusLabel,
96
113
  style: {
97
114
  minWidth: '80px'
98
115
  },
99
- disabled: pending,
116
+ disabled: pending || disabled,
100
117
  className: clsx('byline-coll-restore-button', restore_version_modal_module.button),
101
118
  children: true === pending ? /*#__PURE__*/ jsx(LoaderEllipsis, {
102
119
  size: 42
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+ export {};
@@ -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, useRef, useState } from "react";
3
5
  import { useRouter } from "@tanstack/react-router";
4
6
  import { StatusBadge, renderFormatted } from "@byline/admin/react";
@@ -55,6 +57,7 @@ function SortableTreeRow({ id, dragging, disabled, dragHandleLabel, children })
55
57
  });
56
58
  }
57
59
  const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection, collectionLabels, onMove, listActions })=>{
60
+ const mutation = useDocumentMutationState(void 0);
58
61
  const { t } = useTranslation('byline-admin');
59
62
  const router = useRouter();
60
63
  const toastManager = useToastManager();
@@ -66,12 +69,13 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
66
69
  const [moveController] = useState(()=>createAdminTreeMoveController());
67
70
  const preserveOptimisticForRows = useRef(null);
68
71
  useEffect(()=>{
69
- if (!shouldSyncAdminTreeRows(rows, preserveOptimisticForRows.current, isMoving)) return;
72
+ if (mutation.blocked || !shouldSyncAdminTreeRows(rows, preserveOptimisticForRows.current, isMoving)) return;
70
73
  preserveOptimisticForRows.current = null;
71
74
  setLocalPlaced(rows.filter((r)=>!r.unplaced));
72
75
  }, [
73
76
  rows,
74
- isMoving
77
+ isMoving,
78
+ mutation.blocked
75
79
  ]);
76
80
  const [activeId, setActiveId] = useState(null);
77
81
  const [overId, setOverId] = useState(null);
@@ -98,12 +102,12 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
98
102
  setOffsetLeft(0);
99
103
  };
100
104
  const handleDragStart = ({ active })=>{
101
- if (!moveController.isMoving()) setActiveId(String(active.id));
105
+ if (!mutation.blocked && !moveController.isMoving()) setActiveId(String(active.id));
102
106
  };
103
107
  const handleDragMove = ({ delta })=>setOffsetLeft(delta.x);
104
108
  const handleDragOver = ({ over })=>setOverId(over ? String(over.id) : null);
105
109
  const handleDragEnd = async ({ active, over })=>{
106
- if (moveController.isMoving()) return void resetDnd();
110
+ if (mutation.blocked || moveController.isMoving()) return void resetDnd();
107
111
  const proj = null != over ? getTreeProjection(localPlaced, String(active.id), String(over.id), offsetLeft, INDENT) : null;
108
112
  resetDnd();
109
113
  if (!onMove || null == proj) return;
@@ -125,9 +129,11 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
125
129
  setIsMoving(moveController.isMoving());
126
130
  return;
127
131
  }
132
+ if ('receipt' in outcome) mutation.reportStructure(outcome.receipt);
133
+ const handled = 'error' in outcome ? mutation.report(outcome.error) : null;
128
134
  const effects = getAdminTreeMoveUiEffects(outcome);
129
135
  if (effects.rollback) setLocalPlaced(current);
130
- if (null != effects.structuralFailure) {
136
+ if (!handled && null != effects.structuralFailure) {
131
137
  const conflict = 'conflict' === effects.structuralFailure;
132
138
  toastManager.add({
133
139
  title: t(conflict ? 'collections.list.treeConflictToast' : 'collections.list.reorderFailedToast'),
@@ -202,6 +208,10 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
202
208
  return /*#__PURE__*/ jsx(Section, {
203
209
  children: /*#__PURE__*/ jsxs(Container, {
204
210
  children: [
211
+ /*#__PURE__*/ jsx(DocumentMutationNotice, {
212
+ issue: mutation.issue,
213
+ scheduleNotice: mutation.scheduleNotice
214
+ }),
205
215
  /*#__PURE__*/ jsxs("div", {
206
216
  className: clsx('byline-coll-list-head', tree_list_module.head),
207
217
  children: [
@@ -271,7 +281,7 @@ const TreeListView = ({ rows, columns, workflowStatuses, useAsTitle, collection,
271
281
  return dndEnabled ? /*#__PURE__*/ jsx(SortableTreeRow, {
272
282
  id: row.id,
273
283
  dragging: row.id === activeId,
274
- disabled: isMoving,
284
+ disabled: mutation.blocked || isMoving,
275
285
  dragHandleLabel: t('collections.list.dragHandleAriaLabel'),
276
286
  children: renderCells(row, depth)
277
287
  }, row.id) : /*#__PURE__*/ jsx(Table.Row, {
@@ -40,6 +40,7 @@ interface VersionHistoryRowContext {
40
40
  actionLabel: string;
41
41
  restoreStatusLabel: string;
42
42
  openCompare: () => void;
43
+ mutationsBlocked: boolean;
43
44
  openRestore: () => void;
44
45
  }
45
46
  /**
@@ -66,7 +67,7 @@ export declare function VersionHistoryCore({ data, resourceDefinition, resourceP
66
67
  rowPresentation: VersionHistoryRowPresentation;
67
68
  loadHistoricalVersion: HistoricalVersionLoader;
68
69
  onPageSizeChange: (pageSize: number) => void;
69
- restoreVersion: (versionId: string) => Promise<unknown>;
70
+ restoreVersion: (versionId: string, expectedRevision: number) => Promise<unknown>;
70
71
  onRestoreComplete: () => void | Promise<void>;
71
72
  }): import("react").JSX.Element;
72
73
  export {};
@@ -1,4 +1,6 @@
1
1
  import { Fragment, 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 { Fragment as external_react_Fragment, Suspense, lazy, useState } from "react";
3
5
  import { getDefaultStatus, getWorkflowStatuses } from "@byline/core";
4
6
  import { useTranslation } from "@byline/i18n/react";
@@ -33,6 +35,7 @@ function padRows(value) {
33
35
  }, `empty-row-${index}`));
34
36
  }
35
37
  function VersionHistoryCore({ data, resourceDefinition, resourcePath, documentId, currentDocument, locale, rowPresentation, loadHistoricalVersion, onPageSizeChange, restoreVersion, onRestoreComplete }) {
38
+ const mutation = useDocumentMutationState(currentDocument);
36
39
  const { t } = useTranslation('byline-admin');
37
40
  const [selectedVersion, setSelectedVersion] = useState(null);
38
41
  const [restoreTarget, setRestoreTarget] = useState(null);
@@ -41,6 +44,9 @@ function VersionHistoryCore({ data, resourceDefinition, resourcePath, documentId
41
44
  const restoreStatusLabel = getWorkflowStatuses(resourceDefinition).find((status)=>status.name === restoreStatus)?.label ?? restoreStatus;
42
45
  return /*#__PURE__*/ jsxs(Fragment, {
43
46
  children: [
47
+ /*#__PURE__*/ jsx(DocumentMutationNotice, {
48
+ issue: mutation.issue
49
+ }),
44
50
  /*#__PURE__*/ jsx("div", {
45
51
  className: clsx('byline-coll-history-options', history_module.options),
46
52
  children: /*#__PURE__*/ jsx(RouterPager, {
@@ -85,7 +91,7 @@ function VersionHistoryCore({ data, resourceDefinition, resourcePath, documentId
85
91
  });
86
92
  };
87
93
  const openRestore = ()=>{
88
- if (null == versionId || versionId === currentVersionId) return;
94
+ if (mutation.blocked || null == versionId || versionId === currentVersionId) return;
89
95
  setRestoreTarget({
90
96
  versionId,
91
97
  label: versionLabel,
@@ -119,6 +125,7 @@ function VersionHistoryCore({ data, resourceDefinition, resourcePath, documentId
119
125
  actionLabel,
120
126
  restoreStatusLabel,
121
127
  openCompare,
128
+ mutationsBlocked: mutation.blocked,
122
129
  openRestore
123
130
  })
124
131
  ]
@@ -243,11 +250,13 @@ function VersionHistoryCore({ data, resourceDefinition, resourcePath, documentId
243
250
  ]
244
251
  }),
245
252
  restoreTarget ? /*#__PURE__*/ jsx(RestoreVersionModal, {
253
+ disabled: mutation.blocked,
254
+ onMutationError: mutation.report,
246
255
  versionLabel: restoreTarget.label,
247
256
  versionNumber: restoreTarget.versionNumber,
248
257
  restoreStatusLabel: restoreStatusLabel,
249
258
  onClose: ()=>setRestoreTarget(null),
250
- restoreVersion: ()=>restoreVersion(restoreTarget.versionId),
259
+ restoreVersion: ()=>restoreVersion(restoreTarget.versionId, mutation.expectedRevision()),
251
260
  onRestoreComplete: onRestoreComplete
252
261
  }) : null
253
262
  ]
@@ -0,0 +1,6 @@
1
+ import type { DocumentMutationIssue } from './document-mutation-state.js';
2
+ /** Persistent recovery for list, history and schedule views without local form drafts. */
3
+ export declare function DocumentMutationNotice({ issue, scheduleNotice, }: {
4
+ issue: DocumentMutationIssue | null;
5
+ scheduleNotice?: boolean;
6
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,53 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { useTranslation } from "@byline/i18n/react";
4
+ import { Alert, Button } from "@byline/ui/react";
5
+ import { getAdminRoutePath } from "../routes/admin-path.js";
6
+ function DocumentMutationNotice({ issue, scheduleNotice = false }) {
7
+ const { t } = useTranslation('byline-admin');
8
+ const warning = useRef(null);
9
+ useEffect(()=>{
10
+ if (issue) warning.current?.focus();
11
+ }, [
12
+ issue
13
+ ]);
14
+ if (!issue && !scheduleNotice) return null;
15
+ return /*#__PURE__*/ jsxs("div", {
16
+ ref: warning,
17
+ tabIndex: -1,
18
+ role: "alert",
19
+ "aria-live": "assertive",
20
+ children: [
21
+ issue && /*#__PURE__*/ jsxs(Alert, {
22
+ close: false,
23
+ intent: "warning",
24
+ title: t(`documentConcurrency.${issue}Title`),
25
+ children: [
26
+ /*#__PURE__*/ jsx("p", {
27
+ children: t(`documentConcurrency.${issue}`)
28
+ }),
29
+ 'committed' !== issue && /*#__PURE__*/ jsx(Button, {
30
+ type: "button",
31
+ onClick: ()=>window.location.reload(),
32
+ children: t('documentConcurrency.reloadAction')
33
+ })
34
+ ]
35
+ }),
36
+ scheduleNotice && /*#__PURE__*/ jsxs(Alert, {
37
+ close: false,
38
+ intent: "warning",
39
+ title: t('documentConcurrency.schedulesTitle'),
40
+ children: [
41
+ /*#__PURE__*/ jsx("p", {
42
+ children: t('documentConcurrency.schedules')
43
+ }),
44
+ /*#__PURE__*/ jsx("a", {
45
+ href: getAdminRoutePath('scheduled-publications'),
46
+ children: t('documentConcurrency.reviewSchedules')
47
+ })
48
+ ]
49
+ })
50
+ ]
51
+ });
52
+ }
53
+ export { DocumentMutationNotice };
@@ -0,0 +1,22 @@
1
+ export type DocumentMutationIssue = 'stale' | 'reload' | 'lock' | 'unavailable' | 'committed';
2
+ /** A rejected observation stays frozen until an explicit editor reload. */
3
+ export declare function useDocumentMutationState<T extends {
4
+ id?: unknown;
5
+ versionId?: unknown;
6
+ revision?: unknown;
7
+ }>(document: T | null | undefined): {
8
+ issue: DocumentMutationIssue | null;
9
+ blocked: boolean;
10
+ scheduleNotice: boolean;
11
+ document: T | null | undefined;
12
+ revision: number | undefined;
13
+ expectedRevision: () => number;
14
+ assertWritable: () => void;
15
+ adopt: (receipt: {
16
+ documentId: string;
17
+ revision: number;
18
+ scheduledPublicationsNeedReconfirmation?: boolean;
19
+ }) => void;
20
+ report: (error: unknown) => 'blocked' | 'committed' | null;
21
+ reportStructure: (result: unknown) => void;
22
+ };
@@ -0,0 +1,71 @@
1
+ import { useRef, useState } from "react";
2
+ import { ErrorCodes, getDocumentRevisionValidationDetails, getDocumentStaleDetails, getLockConflictDetails, parseDocumentRevision } from "@byline/core";
3
+ import { getDocumentHookCommittedDetails } from "@byline/core/services";
4
+ import { committedTreeReceipt, structuralReceipt } from "../server-fns/structural-receipt.js";
5
+ function documentMutationIssue(error) {
6
+ if (getDocumentStaleDetails(error)) return 'stale';
7
+ if (getDocumentRevisionValidationDetails(error)) return 'reload';
8
+ if (getLockConflictDetails(error)) return 'lock';
9
+ if (getDocumentHookCommittedDetails(error) || committedTreeReceipt(error)) return 'committed';
10
+ if ('object' == typeof error && null !== error && Reflect.get(error, 'code') === ErrorCodes.NOT_FOUND) return 'unavailable';
11
+ return null;
12
+ }
13
+ function useDocumentMutationState(document) {
14
+ const [issue, setIssue] = useState(null);
15
+ const [scheduleNotice, setScheduleNotice] = useState(false);
16
+ const [, setReceiptRevision] = useState();
17
+ const blockedError = useRef(void 0);
18
+ const observation = useRef({
19
+ identity: document,
20
+ revision: document?.revision
21
+ });
22
+ if (observation.current.identity !== document && void 0 === blockedError.current && document?.revision === observation.current.revision) observation.current = {
23
+ identity: document,
24
+ revision: document?.revision
25
+ };
26
+ const assertWritable = ()=>{
27
+ if (void 0 !== blockedError.current) throw blockedError.current;
28
+ };
29
+ const expectedRevision = ()=>{
30
+ assertWritable();
31
+ return parseDocumentRevision(observation.current.revision);
32
+ };
33
+ const adopt = (receipt)=>{
34
+ if (void 0 !== blockedError.current) return;
35
+ if (document?.id != null && receipt.documentId !== document.id) return;
36
+ observation.current.revision = parseDocumentRevision(receipt.revision);
37
+ setReceiptRevision(receipt.revision);
38
+ if (receipt.scheduledPublicationsNeedReconfirmation) setScheduleNotice(true);
39
+ };
40
+ const reportStructure = (result)=>{
41
+ const receipt = structuralReceipt(result);
42
+ if (receipt) adopt(receipt);
43
+ };
44
+ const report = (error)=>{
45
+ const kind = documentMutationIssue(error);
46
+ if (null === kind) return null;
47
+ if ('committed' === kind) {
48
+ if (void 0 !== blockedError.current) return 'blocked';
49
+ const details = getDocumentHookCommittedDetails(error) ?? committedTreeReceipt(error);
50
+ if (details) adopt(details);
51
+ setIssue(kind);
52
+ return 'committed';
53
+ }
54
+ blockedError.current = error;
55
+ setIssue(kind);
56
+ return 'blocked';
57
+ };
58
+ return {
59
+ issue,
60
+ blocked: null !== issue && 'committed' !== issue,
61
+ scheduleNotice,
62
+ document: observation.current.identity,
63
+ revision: 'number' == typeof observation.current.revision ? observation.current.revision : void 0,
64
+ expectedRevision,
65
+ assertWritable,
66
+ adopt,
67
+ report,
68
+ reportStructure
69
+ };
70
+ }
71
+ export { useDocumentMutationState };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { DocumentMutationNotice } from "../document-mutation-notice.js";
4
+ import { useDocumentMutationState } from "../document-mutation-state.js";
3
5
  import { useMemo, useState } from "react";
4
6
  import { useRouterState } from "@tanstack/react-router";
5
7
  import { deriveScheduledPublicationState } from "@byline/admin/react";
@@ -35,6 +37,7 @@ function ScheduledPublicationsView({ data }) {
35
37
  select: (state)=>state.location
36
38
  });
37
39
  const search = location.search;
40
+ const mutation = useDocumentMutationState(void 0);
38
41
  const [cancelling, setCancelling] = useState(null);
39
42
  const applySearch = (patch)=>{
40
43
  navigate({
@@ -46,13 +49,16 @@ function ScheduledPublicationsView({ data }) {
46
49
  }
47
50
  });
48
51
  };
49
- const cancel = async (collection, documentId)=>{
52
+ const cancel = async (collection, documentId, expectedRevision)=>{
53
+ if (mutation.blocked || cancelling) return;
50
54
  setCancelling(documentId);
51
55
  try {
56
+ mutation.assertWritable();
52
57
  const result = await cancelCollectionDocumentScheduledPublish({
53
58
  data: {
54
59
  collection,
55
- id: documentId
60
+ id: documentId,
61
+ expectedRevision
56
62
  }
57
63
  });
58
64
  const cancelled = 'cancelled' === result.status;
@@ -71,9 +77,10 @@ function ScheduledPublicationsView({ data }) {
71
77
  search: location.search
72
78
  });
73
79
  } catch (error) {
80
+ if (mutation.report(error)) return;
74
81
  toastManager.add({
75
82
  title: t('scheduledPublication.toast.failedTitle'),
76
- description: error.message,
83
+ description: t('documentConcurrency.failed'),
77
84
  data: {
78
85
  intent: 'danger',
79
86
  iconType: 'danger',
@@ -89,6 +96,9 @@ function ScheduledPublicationsView({ data }) {
89
96
  return /*#__PURE__*/ jsx(Section, {
90
97
  children: /*#__PURE__*/ jsxs(Container, {
91
98
  children: [
99
+ /*#__PURE__*/ jsx(DocumentMutationNotice, {
100
+ issue: mutation.issue
101
+ }),
92
102
  /*#__PURE__*/ jsxs("div", {
93
103
  className: clsx('byline-scheduled-list-head', list_module.head),
94
104
  children: [
@@ -264,9 +274,9 @@ function ScheduledPublicationsView({ data }) {
264
274
  size: "xs",
265
275
  type: "button",
266
276
  intent: "danger",
267
- disabled: cancelling === schedule.documentId,
277
+ disabled: mutation.blocked || null !== cancelling,
268
278
  "aria-label": t('scheduledPublication.actions.cancel'),
269
- onClick: ()=>cancel(schedule.collectionPath, schedule.documentId),
279
+ onClick: ()=>cancel(schedule.collectionPath, schedule.documentId, schedule.revision),
270
280
  children: t('common.actions.cancel')
271
281
  })
272
282
  })
@@ -53,7 +53,7 @@ function SingletonHistoryView({ singletonDefinition, adminConfig, data, currentD
53
53
  ]
54
54
  }),
55
55
  auditColSpan: 4,
56
- renderCells: ({ document, versionId, currentVersionId, actorLabel, restoreStatusLabel, openRestore })=>/*#__PURE__*/ jsxs(Fragment, {
56
+ renderCells: ({ document, versionId, currentVersionId, actorLabel, restoreStatusLabel, mutationsBlocked, openRestore })=>/*#__PURE__*/ jsxs(Fragment, {
57
57
  children: [
58
58
  /*#__PURE__*/ jsx(Table.Cell, {
59
59
  className: "byline-singleton-history-status-cell",
@@ -77,6 +77,7 @@ function SingletonHistoryView({ singletonDefinition, adminConfig, data, currentD
77
77
  variant: "outlined",
78
78
  size: "xs",
79
79
  intent: "noeffect",
80
+ disabled: mutationsBlocked,
80
81
  onClick: openRestore,
81
82
  className: clsx('byline-coll-history-restore-button', history_module.restoreButton),
82
83
  title: t('collections.history.restoreButtonTitle', {
@@ -143,8 +144,9 @@ function SingletonHistoryView({ singletonDefinition, adminConfig, data, currentD
143
144
  return historical;
144
145
  },
145
146
  onPageSizeChange: handlePageSizeChange,
146
- restoreVersion: (versionId)=>restoreSingletonVersion({
147
+ restoreVersion: (versionId, expectedRevision)=>restoreSingletonVersion({
147
148
  data: {
149
+ expectedRevision,
148
150
  singleton,
149
151
  versionId
150
152
  }
@@ -5,7 +5,7 @@ type SingletonDocument = Record<string, any> & {
5
5
  status: string;
6
6
  fields: Record<string, any>;
7
7
  };
8
- export declare function SingletonView({ singletonDefinition, adminConfig, document, initialData, locale, contentLocales, defaultContentLocale, }: {
8
+ export declare function SingletonView({ singletonDefinition, adminConfig, document: loadedDocument, initialData, locale, contentLocales, defaultContentLocale, }: {
9
9
  singletonDefinition: SingletonDefinition;
10
10
  adminConfig?: SingletonAdminConfig;
11
11
  document: SingletonDocument | null;