@byline/admin 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.
@@ -23,12 +23,14 @@ import type { PublishedVersionInfo } from './form-renderer'
23
23
  * Unpublish action when a previously-published version is still live.
24
24
  */
25
25
  export const FormStatusDisplay = ({
26
+ disabled = false,
26
27
  initialData,
27
28
  workflowStatuses,
28
29
  publishedVersion,
29
30
  onUnpublish,
30
31
  afterStatusCells,
31
32
  }: {
33
+ disabled?: boolean
32
34
  initialData?: Record<string, any>
33
35
  workflowStatuses?: WorkflowStatus[]
34
36
  publishedVersion?: PublishedVersionInfo | null
@@ -106,7 +108,10 @@ export const FormStatusDisplay = ({
106
108
  {' '}
107
109
  <button
108
110
  type="button"
109
- onClick={onUnpublish}
111
+ disabled={disabled}
112
+ onClick={() => {
113
+ if (!disabled) void onUnpublish().catch(() => {})
114
+ }}
110
115
  className={cx('byline-form-status-unpublish', styles['status-unpublish'])}
111
116
  >
112
117
  {t('common.actions.unpublish')}
@@ -31,6 +31,7 @@ function coerceToString(value: unknown): string {
31
31
  }
32
32
 
33
33
  export interface PathWidgetProps {
34
+ disabled?: boolean
34
35
  /** The collection's `useAsPath` source field name, when configured. */
35
36
  useAsPath: string | undefined
36
37
  /** Collection path, forwarded to the slugifier as context. */
@@ -91,6 +92,7 @@ export const PathWidget = ({
91
92
  mode,
92
93
  slugifier,
93
94
  sourceLocked = false,
95
+ disabled = false,
94
96
  }: PathWidgetProps) => {
95
97
  const { setSystemPath } = useFormContext()
96
98
  const { t } = useTranslation('byline-admin')
@@ -125,18 +127,19 @@ export const PathWidget = ({
125
127
 
126
128
  const handleChange = useCallback(
127
129
  (next: string) => {
130
+ if (disabled) return
128
131
  // Empty string clears the override — server falls back to derive
129
132
  // (create) or sticky (update).
130
133
  setSystemPath(next.length === 0 ? null : next)
131
134
  },
132
- [setSystemPath]
135
+ [disabled, setSystemPath]
133
136
  )
134
137
 
135
138
  const handleRegenerate = useCallback(() => {
136
- if (livePreview.length > 0) {
139
+ if (!disabled && livePreview.length > 0) {
137
140
  setSystemPath(livePreview)
138
141
  }
139
- }, [livePreview, setSystemPath])
142
+ }, [disabled, livePreview, setSystemPath])
140
143
 
141
144
  // Validate live: if the typed value differs from its slugified form,
142
145
  // surface an inline hint without blocking input (mirrors the previous
@@ -184,6 +187,7 @@ export const PathWidget = ({
184
187
  type="button"
185
188
  onClick={handleRegenerate}
186
189
  className={cx('byline-form-path-regenerate', styles.regenerate)}
190
+ disabled={disabled}
187
191
  aria-label={t('pathWidget.regenerateAriaLabel', { field: useAsPath })}
188
192
  >
189
193
  {t('pathWidget.regenerateButton', { field: useAsPath })}
@@ -191,6 +195,7 @@ export const PathWidget = ({
191
195
  )}
192
196
  </div>
193
197
  <Input
198
+ disabled={disabled}
194
199
  id="system-path"
195
200
  name="__systemPath__"
196
201
  value={inputValue}
@@ -110,6 +110,7 @@ function seedScheduleInstant(schedule: ScheduledPublicationInfo | null): Date {
110
110
  }
111
111
 
112
112
  export interface UseScheduledPublicationArgs {
113
+ disabled?: boolean
113
114
  schedule: ScheduledPublicationInfo | null
114
115
  onSchedule?: (input: SchedulePublicationInput) => Promise<void>
115
116
  onConfirm?: () => Promise<void>
@@ -132,6 +133,7 @@ export interface UseScheduledPublicationReturn {
132
133
  }
133
134
 
134
135
  export function useScheduledPublication({
136
+ disabled = false,
135
137
  schedule,
136
138
  onSchedule,
137
139
  onConfirm,
@@ -171,15 +173,16 @@ export function useScheduledPublication({
171
173
  // to be resolved before any of these operations can name a version. Cancel
172
174
  // is exempt: withdrawing a schedule says nothing about content.
173
175
  const openSchedule = useCallback(() => {
176
+ if (disabled) return
174
177
  if (hasUnsavedChanges) {
175
178
  onUnsavedChanges()
176
179
  return
177
180
  }
178
181
  setShowSchedule(true)
179
- }, [hasUnsavedChanges, onUnsavedChanges])
182
+ }, [disabled, hasUnsavedChanges, onUnsavedChanges])
180
183
 
181
184
  const confirm = useCallback(async () => {
182
- if (onConfirm == null) return
185
+ if (disabled || onConfirm == null) return
183
186
  if (hasUnsavedChanges) {
184
187
  onUnsavedChanges()
185
188
  return
@@ -187,37 +190,43 @@ export function useScheduledPublication({
187
190
  setBusy(true)
188
191
  try {
189
192
  await onConfirm()
193
+ } catch {
194
+ // The host reports the failure; keep this view open.
190
195
  } finally {
191
196
  setBusy(false)
192
197
  }
193
- }, [onConfirm, hasUnsavedChanges, onUnsavedChanges])
198
+ }, [disabled, onConfirm, hasUnsavedChanges, onUnsavedChanges])
194
199
 
195
200
  const cancel = useCallback(async () => {
196
- if (onCancel == null) return
201
+ if (disabled || onCancel == null) return
197
202
  setBusy(true)
198
203
  try {
199
204
  await onCancel()
205
+ } catch {
206
+ // The host reports the failure; keep this view open.
200
207
  } finally {
201
208
  setBusy(false)
202
209
  }
203
- }, [onCancel])
210
+ }, [disabled, onCancel])
204
211
 
205
212
  const modal = showSchedule ? (
206
213
  <ScheduleModal
207
214
  schedule={schedule}
208
215
  timeZone={timeZone}
209
216
  onSubmit={async (input) => {
210
- if (onSchedule == null) return
217
+ if (disabled || onSchedule == null) return
211
218
  setBusy(true)
212
219
  try {
213
220
  await onSchedule(input)
214
221
  setShowSchedule(false)
222
+ } catch {
223
+ // The host reports the failure; keep this view open.
215
224
  } finally {
216
225
  setBusy(false)
217
226
  }
218
227
  }}
219
228
  onDismiss={() => setShowSchedule(false)}
220
- busy={busy}
229
+ busy={disabled || busy}
221
230
  />
222
231
  ) : null
223
232
 
@@ -20,6 +20,10 @@ import { RelationPicker } from '../fields/relation/relation-picker.js'
20
20
  import styles from './tree-placement-widget.module.css'
21
21
 
22
22
  export interface TreePlacementWidgetProps {
23
+ disabled?: boolean
24
+ onMutationError?: (error: unknown) => 'blocked' | 'committed' | null | void
25
+ onCommitted?: (receipt: import('@byline/core').StructuralMutationReceipt) => void
26
+ expectedRevision: number
23
27
  /** The collection path (`tree: true`). */
24
28
  collectionPath: string
25
29
  /** The logical id of the document being edited. */
@@ -44,6 +48,10 @@ export interface TreePlacementWidgetProps {
44
48
  */
45
49
  export const TreePlacementWidget = ({
46
50
  collectionPath,
51
+ disabled = false,
52
+ onMutationError,
53
+ onCommitted,
54
+ expectedRevision,
47
55
  documentId,
48
56
  useAsTitle,
49
57
  }: TreePlacementWidgetProps) => {
@@ -99,7 +107,7 @@ export const TreePlacementWidget = ({
99
107
  // placement (including making the node a root) leaves it placed.
100
108
  const place = useCallback(
101
109
  async (parentDocumentId: string | null, optimistic: { id: string; title: string } | null) => {
102
- if (placeTreeNode == null || busy) return
110
+ if (disabled || placeTreeNode == null || busy) return
103
111
  const previousParent = parent
104
112
  const previousPlaced = placed
105
113
  setError(null)
@@ -107,8 +115,15 @@ export const TreePlacementWidget = ({
107
115
  setParent(optimistic)
108
116
  setPlaced(true)
109
117
  try {
110
- await placeTreeNode({ collection: collectionPath, documentId, parentDocumentId })
111
- } catch {
118
+ const result = await placeTreeNode({
119
+ expectedRevision,
120
+ collection: collectionPath,
121
+ documentId,
122
+ parentDocumentId,
123
+ })
124
+ onCommitted?.(result)
125
+ } catch (error) {
126
+ if (onMutationError?.(error) === 'committed') return
112
127
  setParent(previousParent)
113
128
  setPlaced(previousPlaced)
114
129
  setError(t('treeWidget.error'))
@@ -116,7 +131,19 @@ export const TreePlacementWidget = ({
116
131
  setBusy(false)
117
132
  }
118
133
  },
119
- [placeTreeNode, busy, parent, placed, collectionPath, documentId, t]
134
+ [
135
+ disabled,
136
+ onMutationError,
137
+ onCommitted,
138
+ expectedRevision,
139
+ placeTreeNode,
140
+ busy,
141
+ parent,
142
+ placed,
143
+ collectionPath,
144
+ documentId,
145
+ t,
146
+ ]
120
147
  )
121
148
 
122
149
  const handlePick = useCallback(
@@ -156,7 +183,7 @@ export const TreePlacementWidget = ({
156
183
  size="xs"
157
184
  variant="outlined"
158
185
  intent="noeffect"
159
- disabled={loading || busy}
186
+ disabled={disabled || loading || busy}
160
187
  onClick={() => setPickerOpen(true)}
161
188
  >
162
189
  {t('treeWidget.choose')}
@@ -165,7 +192,7 @@ export const TreePlacementWidget = ({
165
192
  <button
166
193
  type="button"
167
194
  className={cx('byline-form-tree-link', styles.link)}
168
- disabled={loading || busy}
195
+ disabled={disabled || loading || busy}
169
196
  onClick={() => place(null, null)}
170
197
  >
171
198
  {t('treeWidget.addToTree')}
@@ -175,7 +202,7 @@ export const TreePlacementWidget = ({
175
202
  <button
176
203
  type="button"
177
204
  className={cx('byline-form-tree-link', styles.link)}
178
- disabled={busy}
205
+ disabled={disabled || busy}
179
206
  onClick={() => place(null, null)}
180
207
  >
181
208
  {t('treeWidget.makeRoot')}