@byline/admin 4.17.0 → 4.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/forms/form-renderer.js +245 -188
- package/dist/forms/form-renderer.module.js +10 -0
- package/dist/forms/form-renderer_module.css +34 -0
- package/package.json +7 -7
- package/src/forms/form-renderer-submit.test.tsx +100 -6
- package/src/forms/form-renderer.module.css +42 -0
- package/src/forms/form-renderer.tsx +277 -199
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { getAdminConfig } from "@byline/core";
|
|
5
5
|
import { useTranslation } from "@byline/i18n/react";
|
|
6
|
-
import { Alert, Button, ComboButton } from "@byline/ui/react";
|
|
6
|
+
import { Alert, Button, ComboButton, LoaderEllipsis } from "@byline/ui/react";
|
|
7
7
|
import clsx from "clsx";
|
|
8
8
|
import { sliceFieldAdmin } from "../fields/field-admin.js";
|
|
9
9
|
import { FieldRenderer } from "../fields/field-renderer.js";
|
|
@@ -33,6 +33,10 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
33
33
|
const [isUploading, setIsUploading] = useState(false);
|
|
34
34
|
const submittingRef = useRef(false);
|
|
35
35
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
36
|
+
const isBusy = isUploading || isSubmitting;
|
|
37
|
+
const formRef = useRef(null);
|
|
38
|
+
const focusBeforeBusyRef = useRef(null);
|
|
39
|
+
const restoreFocusAfterBusyRef = useRef(false);
|
|
36
40
|
const [showUnsavedModal, setShowUnsavedModal] = useState(false);
|
|
37
41
|
const [pendingSystemFieldsSubmit, setPendingSystemFieldsSubmit] = useState(null);
|
|
38
42
|
const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale);
|
|
@@ -106,6 +110,26 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
106
110
|
subscribeMeta,
|
|
107
111
|
getFieldValues
|
|
108
112
|
]);
|
|
113
|
+
useEffect(()=>{
|
|
114
|
+
if (isBusy || !restoreFocusAfterBusyRef.current) return;
|
|
115
|
+
restoreFocusAfterBusyRef.current = false;
|
|
116
|
+
const original = focusBeforeBusyRef.current;
|
|
117
|
+
focusBeforeBusyRef.current = null;
|
|
118
|
+
const originalCanReceiveFocus = original?.isConnected === true && !original.matches(':disabled, [aria-disabled="true"]');
|
|
119
|
+
const target = originalCanReceiveFocus ? original : formRef.current?.querySelector('input:not(:disabled), textarea:not(:disabled), select:not(:disabled), button:not(:disabled), [tabindex]:not([tabindex="-1"])');
|
|
120
|
+
target?.focus({
|
|
121
|
+
preventScroll: true
|
|
122
|
+
});
|
|
123
|
+
}, [
|
|
124
|
+
isBusy
|
|
125
|
+
]);
|
|
126
|
+
const captureFocusBeforeBusy = useCallback(()=>{
|
|
127
|
+
if (null != focusBeforeBusyRef.current) return;
|
|
128
|
+
const activeElement = document.activeElement;
|
|
129
|
+
if (!(activeElement instanceof HTMLElement) || !formRef.current?.contains(activeElement)) return;
|
|
130
|
+
focusBeforeBusyRef.current = activeElement;
|
|
131
|
+
restoreFocusAfterBusyRef.current = true;
|
|
132
|
+
}, []);
|
|
109
133
|
const handleCancel = ()=>{
|
|
110
134
|
if (onCancel && 'function' == typeof onCancel) onCancel();
|
|
111
135
|
};
|
|
@@ -113,6 +137,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
113
137
|
if ('function' != typeof onSubmit) return;
|
|
114
138
|
if (submittingRef.current) return;
|
|
115
139
|
submittingRef.current = true;
|
|
140
|
+
captureFocusBeforeBusy();
|
|
116
141
|
setIsSubmitting(true);
|
|
117
142
|
try {
|
|
118
143
|
await onSubmit(payload);
|
|
@@ -122,6 +147,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
122
147
|
setIsSubmitting(false);
|
|
123
148
|
}
|
|
124
149
|
}, [
|
|
150
|
+
captureFocusBeforeBusy,
|
|
125
151
|
onSubmit,
|
|
126
152
|
resetHasChanges
|
|
127
153
|
]);
|
|
@@ -137,6 +163,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
137
163
|
if (allErrors.length > 0) return void console.error('Form validation failed:', allErrors);
|
|
138
164
|
const pendingUploads = getPendingUploads();
|
|
139
165
|
if (pendingUploads.size > 0) {
|
|
166
|
+
captureFocusBeforeBusy();
|
|
140
167
|
setIsUploading(true);
|
|
141
168
|
try {
|
|
142
169
|
const uploadResult = await executeUploadsWithProgress(pendingUploads, uploadField, ({ fieldPath, status })=>{
|
|
@@ -245,196 +272,226 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
245
272
|
]
|
|
246
273
|
}, `tabset:${set.name}`);
|
|
247
274
|
};
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
noValidate: true,
|
|
251
|
-
onSubmit: handleSubmit,
|
|
252
|
-
className: clsx('byline-form', form_renderer_module.form),
|
|
253
|
-
inert: isUploading ? true : void 0,
|
|
254
|
-
"aria-busy": isUploading,
|
|
275
|
+
const busyAnnouncement = isUploading ? t('forms.actions.uploading') : isSubmitting ? t('forms.actions.saving') : '';
|
|
276
|
+
return /*#__PURE__*/ jsxs(Fragment, {
|
|
255
277
|
children: [
|
|
256
|
-
/*#__PURE__*/
|
|
257
|
-
className: clsx('byline-form-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
children: computedHeading
|
|
262
|
-
}),
|
|
263
|
-
headerSlot
|
|
264
|
-
]
|
|
278
|
+
/*#__PURE__*/ jsx("span", {
|
|
279
|
+
className: clsx('byline-form-busy-status', form_renderer_module["busy-status"]),
|
|
280
|
+
role: "status",
|
|
281
|
+
"aria-live": "polite",
|
|
282
|
+
children: busyAnnouncement
|
|
265
283
|
}),
|
|
266
|
-
/*#__PURE__*/
|
|
267
|
-
|
|
268
|
-
children:
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
284
|
+
/*#__PURE__*/ jsx("div", {
|
|
285
|
+
"aria-busy": isBusy,
|
|
286
|
+
children: /*#__PURE__*/ jsxs("form", {
|
|
287
|
+
ref: formRef,
|
|
288
|
+
method: "post",
|
|
289
|
+
noValidate: true,
|
|
290
|
+
onSubmit: handleSubmit,
|
|
291
|
+
className: clsx('byline-form', form_renderer_module.form),
|
|
292
|
+
inert: isBusy ? true : void 0,
|
|
293
|
+
children: [
|
|
294
|
+
/*#__PURE__*/ jsxs("div", {
|
|
295
|
+
className: clsx('byline-form-heading-row', form_renderer_module["heading-row"]),
|
|
296
|
+
children: [
|
|
297
|
+
/*#__PURE__*/ jsx("h1", {
|
|
298
|
+
className: clsx('byline-form-heading', form_renderer_module.heading),
|
|
299
|
+
children: computedHeading
|
|
300
|
+
}),
|
|
301
|
+
headerSlot
|
|
302
|
+
]
|
|
303
|
+
}),
|
|
304
|
+
/*#__PURE__*/ jsxs("div", {
|
|
305
|
+
className: clsx('byline-form-status-bar', form_renderer_module["status-bar"]),
|
|
306
|
+
children: [
|
|
307
|
+
/*#__PURE__*/ jsx("div", {
|
|
308
|
+
className: clsx('byline-form-status-details', form_renderer_module["status-details"]),
|
|
309
|
+
children: /*#__PURE__*/ jsx(FormStatusDisplay, {
|
|
310
|
+
initialData: initialData,
|
|
311
|
+
workflowStatuses: workflowStatuses,
|
|
312
|
+
publishedVersion: publishedVersion,
|
|
313
|
+
onUnpublish: onUnpublish,
|
|
314
|
+
afterStatusCells: /*#__PURE__*/ jsx(ScheduledPublicationCell, {
|
|
315
|
+
state: scheduling.state,
|
|
316
|
+
timeZone: scheduling.timeZone
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
}),
|
|
320
|
+
/*#__PURE__*/ jsxs("div", {
|
|
321
|
+
className: clsx('byline-form-actions', form_renderer_module.actions),
|
|
322
|
+
children: [
|
|
323
|
+
/*#__PURE__*/ jsx(Button, {
|
|
324
|
+
className: clsx('byline-form-actions-button', form_renderer_module["actions-button"]),
|
|
325
|
+
size: "sm",
|
|
326
|
+
intent: "noeffect",
|
|
327
|
+
type: "button",
|
|
328
|
+
onClick: handleCancel,
|
|
329
|
+
children: false === hasChanges ? t('common.actions.close') : t('common.actions.cancel')
|
|
330
|
+
}),
|
|
331
|
+
/*#__PURE__*/ jsx(Button, {
|
|
332
|
+
className: clsx('byline-form-actions-button', form_renderer_module["actions-button"]),
|
|
333
|
+
size: "sm",
|
|
334
|
+
type: "submit",
|
|
335
|
+
disabled: false === hasChanges || isUploading || isSubmitting,
|
|
336
|
+
"aria-label": isSubmitting ? t('common.actions.save') : void 0,
|
|
337
|
+
children: isUploading ? t('forms.actions.uploading') : /*#__PURE__*/ jsxs("span", {
|
|
338
|
+
className: clsx('byline-form-save-content', form_renderer_module["save-content"]),
|
|
339
|
+
children: [
|
|
340
|
+
/*#__PURE__*/ jsx("span", {
|
|
341
|
+
className: clsx('byline-form-save-label', form_renderer_module["save-label"], isSubmitting && form_renderer_module["save-label-hidden"]),
|
|
342
|
+
children: t('common.actions.save')
|
|
343
|
+
}),
|
|
344
|
+
isSubmitting ? /*#__PURE__*/ jsx("span", {
|
|
345
|
+
className: clsx('byline-form-save-loader', form_renderer_module["save-loader"]),
|
|
346
|
+
children: /*#__PURE__*/ jsx(LoaderEllipsis, {
|
|
347
|
+
size: 28,
|
|
348
|
+
"aria-hidden": "true"
|
|
349
|
+
})
|
|
350
|
+
}) : null
|
|
351
|
+
]
|
|
352
|
+
})
|
|
353
|
+
}),
|
|
354
|
+
primaryStatus && onStatusChange && /*#__PURE__*/ jsx("div", {
|
|
355
|
+
className: clsx('byline-form-actions-status-wrap', form_renderer_module["actions-status-wrap"]),
|
|
356
|
+
children: /*#__PURE__*/ jsx(ComboButton, {
|
|
357
|
+
buttonClassName: clsx('byline-form-actions-combo-button', form_renderer_module["actions-combo-button"]),
|
|
358
|
+
triggerClassName: clsx('byline-form-actions-combo-trigger', form_renderer_module["actions-combo-trigger"]),
|
|
359
|
+
options: secondaryStatuses.map((s)=>({
|
|
360
|
+
label: isTerminal ? t('forms.actions.revertTo', {
|
|
361
|
+
label: s.label ?? s.name
|
|
362
|
+
}) : s.verb ?? s.label ?? s.name,
|
|
363
|
+
value: s.name
|
|
364
|
+
})),
|
|
365
|
+
sideOffset: 5,
|
|
366
|
+
size: "sm",
|
|
367
|
+
type: "button",
|
|
368
|
+
intent: isTerminal ? 'info' : 'success',
|
|
369
|
+
disabled: statusBusy,
|
|
370
|
+
onOptionSelect: async (value)=>{
|
|
371
|
+
if (hasChanges) return void setShowUnsavedModal(true);
|
|
372
|
+
setStatusBusy(true);
|
|
373
|
+
try {
|
|
374
|
+
await onStatusChange(value);
|
|
375
|
+
} finally{
|
|
376
|
+
setStatusBusy(false);
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
onButtonClick: isTerminal ? void 0 : async ()=>{
|
|
380
|
+
if (hasChanges) return void setShowUnsavedModal(true);
|
|
381
|
+
setStatusBusy(true);
|
|
382
|
+
try {
|
|
383
|
+
await onStatusChange(primaryStatus.name);
|
|
384
|
+
} finally{
|
|
385
|
+
setStatusBusy(false);
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
children: statusBusy ? '...' : isTerminal ? primaryStatus.label ?? primaryStatus.name : primaryStatus.verb ?? primaryStatus.label ?? primaryStatus.name
|
|
389
|
+
})
|
|
390
|
+
}),
|
|
391
|
+
/*#__PURE__*/ jsx(DocumentActions, {
|
|
392
|
+
publishedVersion: publishedVersion,
|
|
393
|
+
onUnpublish: onUnpublish,
|
|
394
|
+
onDelete: onDelete,
|
|
395
|
+
onDuplicate: onDuplicate,
|
|
396
|
+
sourceTitle: null != useAsTitle && null != initialData ? initialData[useAsTitle] : null,
|
|
397
|
+
onCopyToLocale: onCopyToLocale,
|
|
398
|
+
sourceLocale: contentLocale,
|
|
399
|
+
contentLocales: contentLocales,
|
|
400
|
+
hasUnsavedChanges: hasChanges,
|
|
401
|
+
onUnsavedChanges: ()=>setShowUnsavedModal(true),
|
|
402
|
+
onDeleteLocale: onDeleteLocale,
|
|
403
|
+
defaultLocale: defaultLocale,
|
|
404
|
+
availableLocales: initialData?._availableVersionLocales,
|
|
405
|
+
scheduledPublicationState: scheduling.state,
|
|
406
|
+
onSchedulePublication: scheduling.openSchedule,
|
|
407
|
+
onConfirmScheduledPublication: scheduling.confirm,
|
|
408
|
+
onCancelScheduledPublication: scheduling.cancel
|
|
409
|
+
})
|
|
410
|
+
]
|
|
335
411
|
})
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
412
|
+
]
|
|
413
|
+
}),
|
|
414
|
+
/*#__PURE__*/ jsx(ScheduledPublicationNotice, {
|
|
415
|
+
state: scheduling.state,
|
|
416
|
+
timeZone: scheduling.timeZone,
|
|
417
|
+
busy: scheduling.busy,
|
|
418
|
+
onConfirm: scheduling.confirm,
|
|
419
|
+
onReschedule: scheduling.openSchedule,
|
|
420
|
+
onCancel: scheduling.cancel
|
|
421
|
+
}),
|
|
422
|
+
scheduling.modal,
|
|
423
|
+
restoreWarnings && restoreWarnings.length > 0 && /*#__PURE__*/ jsxs(Alert, {
|
|
424
|
+
className: "m-0 mt-4",
|
|
425
|
+
intent: "warning",
|
|
426
|
+
icon: true,
|
|
427
|
+
close: false,
|
|
428
|
+
title: t('forms.restoreWarnings.title'),
|
|
429
|
+
children: [
|
|
430
|
+
/*#__PURE__*/ jsx("p", {
|
|
431
|
+
children: t('forms.restoreWarnings.body', {
|
|
432
|
+
count: restoreWarnings.length
|
|
433
|
+
})
|
|
434
|
+
}),
|
|
435
|
+
/*#__PURE__*/ jsx("ul", {
|
|
436
|
+
children: restoreWarnings.map((w)=>/*#__PURE__*/ jsx("li", {
|
|
437
|
+
children: w
|
|
438
|
+
}, w))
|
|
439
|
+
})
|
|
440
|
+
]
|
|
441
|
+
}),
|
|
442
|
+
/*#__PURE__*/ jsxs("div", {
|
|
443
|
+
className: clsx('byline-form-layout', form_renderer_module.layout),
|
|
444
|
+
children: [
|
|
445
|
+
/*#__PURE__*/ jsx("div", {
|
|
446
|
+
className: clsx('byline-form-content', form_renderer_module.content),
|
|
447
|
+
children: layout.main.map((name)=>renderItem(name))
|
|
448
|
+
}),
|
|
449
|
+
/*#__PURE__*/ jsxs("div", {
|
|
450
|
+
className: clsx('byline-form-sidebar', form_renderer_module.sidebar),
|
|
451
|
+
children: [
|
|
452
|
+
showPath && (useAsPath || 'string' == typeof initialData?.path && initialData.path.length > 0) && /*#__PURE__*/ jsx(PathWidget, {
|
|
453
|
+
useAsPath: useAsPath,
|
|
454
|
+
collectionPath: collectionPath ?? '',
|
|
455
|
+
defaultLocale: defaultLocale,
|
|
456
|
+
activeLocale: contentLocale,
|
|
457
|
+
mode: mode,
|
|
458
|
+
slugifier: pathSlugifier,
|
|
459
|
+
sourceLocked: pathSourceLocked
|
|
460
|
+
}),
|
|
461
|
+
tree && 'edit' === mode && 'string' == typeof initialData?.id && /*#__PURE__*/ jsx(TreePlacementWidget, {
|
|
462
|
+
collectionPath: collectionPath ?? '',
|
|
463
|
+
documentId: initialData.id,
|
|
464
|
+
useAsTitle: useAsTitle
|
|
465
|
+
}),
|
|
466
|
+
advertiseLocales && /*#__PURE__*/ jsx(AvailableLocalesWidget, {
|
|
467
|
+
contentLocales: contentLocales ?? [],
|
|
468
|
+
availableVersionLocales: initialData?._availableVersionLocales ?? []
|
|
469
|
+
}),
|
|
470
|
+
(layout.sidebar ?? []).map((name)=>renderItem(name))
|
|
471
|
+
]
|
|
472
|
+
})
|
|
473
|
+
]
|
|
474
|
+
}),
|
|
475
|
+
showUnsavedModal && /*#__PURE__*/ jsx(UnsavedChangesModal, {
|
|
476
|
+
onClose: ()=>setShowUnsavedModal(false)
|
|
477
|
+
}),
|
|
478
|
+
null != pendingSystemFieldsSubmit && /*#__PURE__*/ jsx(SystemFieldsConfirmModal, {
|
|
479
|
+
contentDirty: pendingSystemFieldsSubmit.contentDirty,
|
|
480
|
+
pathDirty: pendingSystemFieldsSubmit.pathDirty,
|
|
481
|
+
availableLocalesDirty: pendingSystemFieldsSubmit.availableLocalesDirty,
|
|
482
|
+
onCancel: ()=>setPendingSystemFieldsSubmit(null),
|
|
483
|
+
onConfirm: ()=>{
|
|
484
|
+
const payload = pendingSystemFieldsSubmit;
|
|
485
|
+
setPendingSystemFieldsSubmit(null);
|
|
486
|
+
submitPayload(payload);
|
|
487
|
+
}
|
|
488
|
+
}),
|
|
489
|
+
guard.isBlocked && /*#__PURE__*/ jsx(NavigationGuardModal, {
|
|
490
|
+
onStay: guard.stay,
|
|
491
|
+
onProceed: guard.proceed
|
|
379
492
|
})
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
children: restoreWarnings.map((w)=>/*#__PURE__*/ jsx("li", {
|
|
383
|
-
children: w
|
|
384
|
-
}, w))
|
|
385
|
-
})
|
|
386
|
-
]
|
|
387
|
-
}),
|
|
388
|
-
/*#__PURE__*/ jsxs("div", {
|
|
389
|
-
className: clsx('byline-form-layout', form_renderer_module.layout),
|
|
390
|
-
children: [
|
|
391
|
-
/*#__PURE__*/ jsx("div", {
|
|
392
|
-
className: clsx('byline-form-content', form_renderer_module.content),
|
|
393
|
-
children: layout.main.map((name)=>renderItem(name))
|
|
394
|
-
}),
|
|
395
|
-
/*#__PURE__*/ jsxs("div", {
|
|
396
|
-
className: clsx('byline-form-sidebar', form_renderer_module.sidebar),
|
|
397
|
-
children: [
|
|
398
|
-
showPath && (useAsPath || 'string' == typeof initialData?.path && initialData.path.length > 0) && /*#__PURE__*/ jsx(PathWidget, {
|
|
399
|
-
useAsPath: useAsPath,
|
|
400
|
-
collectionPath: collectionPath ?? '',
|
|
401
|
-
defaultLocale: defaultLocale,
|
|
402
|
-
activeLocale: contentLocale,
|
|
403
|
-
mode: mode,
|
|
404
|
-
slugifier: pathSlugifier,
|
|
405
|
-
sourceLocked: pathSourceLocked
|
|
406
|
-
}),
|
|
407
|
-
tree && 'edit' === mode && 'string' == typeof initialData?.id && /*#__PURE__*/ jsx(TreePlacementWidget, {
|
|
408
|
-
collectionPath: collectionPath ?? '',
|
|
409
|
-
documentId: initialData.id,
|
|
410
|
-
useAsTitle: useAsTitle
|
|
411
|
-
}),
|
|
412
|
-
advertiseLocales && /*#__PURE__*/ jsx(AvailableLocalesWidget, {
|
|
413
|
-
contentLocales: contentLocales ?? [],
|
|
414
|
-
availableVersionLocales: initialData?._availableVersionLocales ?? []
|
|
415
|
-
}),
|
|
416
|
-
(layout.sidebar ?? []).map((name)=>renderItem(name))
|
|
417
|
-
]
|
|
418
|
-
})
|
|
419
|
-
]
|
|
420
|
-
}),
|
|
421
|
-
showUnsavedModal && /*#__PURE__*/ jsx(UnsavedChangesModal, {
|
|
422
|
-
onClose: ()=>setShowUnsavedModal(false)
|
|
423
|
-
}),
|
|
424
|
-
null != pendingSystemFieldsSubmit && /*#__PURE__*/ jsx(SystemFieldsConfirmModal, {
|
|
425
|
-
contentDirty: pendingSystemFieldsSubmit.contentDirty,
|
|
426
|
-
pathDirty: pendingSystemFieldsSubmit.pathDirty,
|
|
427
|
-
availableLocalesDirty: pendingSystemFieldsSubmit.availableLocalesDirty,
|
|
428
|
-
onCancel: ()=>setPendingSystemFieldsSubmit(null),
|
|
429
|
-
onConfirm: ()=>{
|
|
430
|
-
const payload = pendingSystemFieldsSubmit;
|
|
431
|
-
setPendingSystemFieldsSubmit(null);
|
|
432
|
-
submitPayload(payload);
|
|
433
|
-
}
|
|
434
|
-
}),
|
|
435
|
-
guard.isBlocked && /*#__PURE__*/ jsx(NavigationGuardModal, {
|
|
436
|
-
onStay: guard.stay,
|
|
437
|
-
onProceed: guard.proceed
|
|
493
|
+
]
|
|
494
|
+
})
|
|
438
495
|
})
|
|
439
496
|
]
|
|
440
497
|
});
|
|
@@ -24,6 +24,16 @@ const form_renderer_module = {
|
|
|
24
24
|
actions: "actions-AZBIdR",
|
|
25
25
|
"actions-button": "actions-button-VIwbST",
|
|
26
26
|
actionsButton: "actions-button-VIwbST",
|
|
27
|
+
"busy-status": "busy-status-kzSMVH",
|
|
28
|
+
busyStatus: "busy-status-kzSMVH",
|
|
29
|
+
"save-content": "save-content-bc8XeC",
|
|
30
|
+
saveContent: "save-content-bc8XeC",
|
|
31
|
+
"save-label": "save-label-iJw9DA",
|
|
32
|
+
saveLabel: "save-label-iJw9DA",
|
|
33
|
+
"save-label-hidden": "save-label-hidden-Wct46I",
|
|
34
|
+
saveLabelHidden: "save-label-hidden-Wct46I",
|
|
35
|
+
"save-loader": "save-loader-g2wcnV",
|
|
36
|
+
saveLoader: "save-loader-g2wcnV",
|
|
27
37
|
"actions-status-wrap": "actions-status-wrap-hSvlps",
|
|
28
38
|
actionsStatusWrap: "actions-status-wrap-hSvlps",
|
|
29
39
|
"actions-combo-button": "actions-combo-button-XvqHCC",
|
|
@@ -141,6 +141,40 @@
|
|
|
141
141
|
min-height: 28px;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
:is(.busy-status-kzSMVH, .byline-form-busy-status) {
|
|
145
|
+
clip: rect(0 0 0 0);
|
|
146
|
+
clip-path: inset(50%);
|
|
147
|
+
white-space: nowrap;
|
|
148
|
+
border: 0;
|
|
149
|
+
width: 1px;
|
|
150
|
+
height: 1px;
|
|
151
|
+
margin: -1px;
|
|
152
|
+
padding: 0;
|
|
153
|
+
position: absolute;
|
|
154
|
+
overflow: hidden;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
:is(.save-content-bc8XeC, .byline-form-save-content) {
|
|
158
|
+
display: inline-block;
|
|
159
|
+
position: relative;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
:is(.save-label-iJw9DA, .byline-form-save-label) {
|
|
163
|
+
display: inline-block;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.save-label-hidden-Wct46I {
|
|
167
|
+
visibility: hidden;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
:is(.save-loader-g2wcnV, .byline-form-save-loader) {
|
|
171
|
+
display: inline-flex;
|
|
172
|
+
position: absolute;
|
|
173
|
+
top: 50%;
|
|
174
|
+
left: 50%;
|
|
175
|
+
transform: translate(-50%, -50%);
|
|
176
|
+
}
|
|
177
|
+
|
|
144
178
|
:is(.actions-status-wrap-hSvlps, .byline-form-actions-status-wrap) {
|
|
145
179
|
z-index: 10;
|
|
146
180
|
position: relative;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/admin",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.19.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -183,12 +183,12 @@
|
|
|
183
183
|
"react-diff-viewer-continued": "^4.4.0",
|
|
184
184
|
"uuid": "^14.0.2",
|
|
185
185
|
"zod": "^4.4.3",
|
|
186
|
-
"@byline/
|
|
187
|
-
"@byline/
|
|
188
|
-
"@byline/
|
|
189
|
-
"@byline/
|
|
190
|
-
"@byline/
|
|
191
|
-
"@byline/
|
|
186
|
+
"@byline/analytics": "4.19.0",
|
|
187
|
+
"@byline/analytics-agent": "4.19.0",
|
|
188
|
+
"@byline/auth": "4.19.0",
|
|
189
|
+
"@byline/ui": "4.19.0",
|
|
190
|
+
"@byline/i18n": "4.19.0",
|
|
191
|
+
"@byline/core": "4.19.0"
|
|
192
192
|
},
|
|
193
193
|
"peerDependencies": {
|
|
194
194
|
"react": "^19.0.0",
|
|
@@ -129,6 +129,16 @@ const submitForm = () => {
|
|
|
129
129
|
})
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
const deferred = () => {
|
|
133
|
+
let resolve: () => void = () => {}
|
|
134
|
+
let reject: (reason?: unknown) => void = () => {}
|
|
135
|
+
const promise = new Promise<void>((resolvePromise, rejectPromise) => {
|
|
136
|
+
resolve = resolvePromise
|
|
137
|
+
reject = rejectPromise
|
|
138
|
+
})
|
|
139
|
+
return { promise, resolve, reject }
|
|
140
|
+
}
|
|
141
|
+
|
|
132
142
|
describe('FormRenderer submit contract', () => {
|
|
133
143
|
const render = (props: Record<string, unknown>) =>
|
|
134
144
|
renderInProvider(<FormRenderer {...(props as any)} />)
|
|
@@ -169,13 +179,97 @@ describe('FormRenderer submit contract', () => {
|
|
|
169
179
|
expect(container.textContent).toContain('Cancel')
|
|
170
180
|
})
|
|
171
181
|
|
|
172
|
-
it('
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
182
|
+
it('shows a named busy indicator until onSubmit resolves', async () => {
|
|
183
|
+
const submission = deferred()
|
|
184
|
+
render({ ...baseProps, onSubmit: () => submission.promise })
|
|
185
|
+
typeIntoTitle('Hello')
|
|
186
|
+
submitForm()
|
|
187
|
+
await act(async () => {})
|
|
188
|
+
|
|
189
|
+
const form = container.querySelector('form')
|
|
190
|
+
const busyRegion = form?.parentElement
|
|
191
|
+
const liveStatus = container.querySelector('[role="status"]')
|
|
192
|
+
const saveButton = container.querySelector<HTMLButtonElement>('button[type="submit"]')
|
|
193
|
+
expect(busyRegion?.getAttribute('aria-busy')).toBe('true')
|
|
194
|
+
expect(form?.getAttribute('aria-busy')).toBeNull()
|
|
195
|
+
expect(form?.hasAttribute('inert')).toBe(true)
|
|
196
|
+
expect(liveStatus?.textContent).toBe('Saving…')
|
|
197
|
+
expect(busyRegion?.contains(liveStatus)).toBe(false)
|
|
198
|
+
expect(saveButton?.getAttribute('aria-label')).toBe('Save')
|
|
199
|
+
expect(saveButton?.querySelector('.byline-loader-ellipsis')).not.toBeNull()
|
|
200
|
+
expect(saveButton?.querySelector('.byline-form-save-label')?.textContent).toBe('Save')
|
|
201
|
+
|
|
202
|
+
await act(async () => {
|
|
203
|
+
submission.resolve()
|
|
204
|
+
await submission.promise
|
|
176
205
|
})
|
|
206
|
+
|
|
207
|
+
expect(busyRegion?.getAttribute('aria-busy')).toBe('false')
|
|
208
|
+
expect(form?.hasAttribute('inert')).toBe(false)
|
|
209
|
+
expect(liveStatus?.textContent).toBe('')
|
|
210
|
+
expect(saveButton?.getAttribute('aria-label')).toBeNull()
|
|
211
|
+
expect(saveButton?.querySelector('.byline-loader-ellipsis')).toBeNull()
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('hides the busy indicator when onSubmit rejects', async () => {
|
|
215
|
+
const submission = deferred()
|
|
216
|
+
render({ ...baseProps, onSubmit: () => submission.promise })
|
|
217
|
+
typeIntoTitle('Hello')
|
|
218
|
+
submitForm()
|
|
219
|
+
await act(async () => {})
|
|
220
|
+
|
|
221
|
+
const form = container.querySelector('form')
|
|
222
|
+
const busyRegion = form?.parentElement
|
|
223
|
+
const saveButton = container.querySelector<HTMLButtonElement>('button[type="submit"]')
|
|
224
|
+
expect(busyRegion?.getAttribute('aria-busy')).toBe('true')
|
|
225
|
+
expect(form?.getAttribute('aria-busy')).toBeNull()
|
|
226
|
+
expect(saveButton?.querySelector('.byline-loader-ellipsis')).not.toBeNull()
|
|
227
|
+
|
|
228
|
+
await act(async () => {
|
|
229
|
+
submission.reject(new Error('save failed'))
|
|
230
|
+
try {
|
|
231
|
+
await submission.promise
|
|
232
|
+
} catch {
|
|
233
|
+
// FormRenderer intentionally handles host-reported submission failures.
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
expect(form?.getAttribute('aria-busy')).toBeNull()
|
|
238
|
+
expect(busyRegion?.getAttribute('aria-busy')).toBe('false')
|
|
239
|
+
expect(form?.hasAttribute('inert')).toBe(false)
|
|
240
|
+
expect(saveButton?.getAttribute('aria-label')).toBeNull()
|
|
241
|
+
expect(saveButton?.querySelector('.byline-loader-ellipsis')).toBeNull()
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('restores keyboard focus after the inert submitting window closes', async () => {
|
|
245
|
+
const submission = deferred()
|
|
246
|
+
render({ ...baseProps, onSubmit: () => submission.promise })
|
|
247
|
+
typeIntoTitle('Hello')
|
|
248
|
+
const input = container.querySelector<HTMLInputElement>('input[name="title"]')
|
|
249
|
+
if (input == null) throw new Error('title input not found')
|
|
250
|
+
input.focus()
|
|
251
|
+
expect(document.activeElement).toBe(input)
|
|
252
|
+
|
|
253
|
+
submitForm()
|
|
254
|
+
await act(async () => {})
|
|
255
|
+
|
|
256
|
+
document.body.tabIndex = -1
|
|
257
|
+
document.body.focus()
|
|
258
|
+
expect(document.activeElement).toBe(document.body)
|
|
259
|
+
|
|
260
|
+
await act(async () => {
|
|
261
|
+
submission.resolve()
|
|
262
|
+
await submission.promise
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
expect(document.activeElement).toBe(input)
|
|
266
|
+
document.body.removeAttribute('tabindex')
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('ignores a second submit while the first is still in flight', async () => {
|
|
270
|
+
const submission = deferred()
|
|
177
271
|
const onSubmit = vi.fn(async () => {
|
|
178
|
-
await
|
|
272
|
+
await submission.promise
|
|
179
273
|
})
|
|
180
274
|
render({ ...baseProps, onSubmit })
|
|
181
275
|
typeIntoTitle('Hello')
|
|
@@ -187,7 +281,7 @@ describe('FormRenderer submit contract', () => {
|
|
|
187
281
|
await act(async () => {})
|
|
188
282
|
expect(onSubmit).toHaveBeenCalledTimes(1)
|
|
189
283
|
await act(async () => {
|
|
190
|
-
|
|
284
|
+
submission.resolve()
|
|
191
285
|
})
|
|
192
286
|
expect(onSubmit).toHaveBeenCalledTimes(1)
|
|
193
287
|
})
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
* .byline-form-actions-status-wrap — relative positioned wrapper around the ComboButton
|
|
21
21
|
* .byline-form-actions-combo-button — ComboButton's main button minimums
|
|
22
22
|
* .byline-form-actions-combo-trigger — ComboButton's trigger button minimums
|
|
23
|
+
* .byline-form-busy-status — visually-hidden save/upload announcement
|
|
24
|
+
* .byline-form-save-content — stable-width Save label/loader wrapper
|
|
25
|
+
* .byline-form-save-label — visible label that determines button width
|
|
26
|
+
* .byline-form-save-loader — centered progress overlay
|
|
23
27
|
*
|
|
24
28
|
* .byline-form-layout — main + sidebar grid wrapper
|
|
25
29
|
* .byline-form-content — main column
|
|
@@ -206,6 +210,44 @@
|
|
|
206
210
|
min-height: 28px;
|
|
207
211
|
}
|
|
208
212
|
|
|
213
|
+
.busy-status,
|
|
214
|
+
:global(.byline-form-busy-status) {
|
|
215
|
+
position: absolute;
|
|
216
|
+
overflow: hidden;
|
|
217
|
+
width: 1px;
|
|
218
|
+
height: 1px;
|
|
219
|
+
padding: 0;
|
|
220
|
+
border: 0;
|
|
221
|
+
margin: -1px;
|
|
222
|
+
clip: rect(0 0 0 0);
|
|
223
|
+
clip-path: inset(50%);
|
|
224
|
+
white-space: nowrap;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.save-content,
|
|
228
|
+
:global(.byline-form-save-content) {
|
|
229
|
+
position: relative;
|
|
230
|
+
display: inline-block;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.save-label,
|
|
234
|
+
:global(.byline-form-save-label) {
|
|
235
|
+
display: inline-block;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.save-label-hidden {
|
|
239
|
+
visibility: hidden;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.save-loader,
|
|
243
|
+
:global(.byline-form-save-loader) {
|
|
244
|
+
position: absolute;
|
|
245
|
+
top: 50%;
|
|
246
|
+
left: 50%;
|
|
247
|
+
display: inline-flex;
|
|
248
|
+
transform: translate(-50%, -50%);
|
|
249
|
+
}
|
|
250
|
+
|
|
209
251
|
.actions-status-wrap,
|
|
210
252
|
:global(.byline-form-actions-status-wrap) {
|
|
211
253
|
position: relative;
|
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
import { getAdminConfig } from '@byline/core'
|
|
22
22
|
import type { DocumentPatch } from '@byline/core/patches'
|
|
23
23
|
import { useTranslation } from '@byline/i18n/react'
|
|
24
|
-
import { Alert, Button, ComboButton } from '@byline/ui/react'
|
|
24
|
+
import { Alert, Button, ComboButton, LoaderEllipsis } from '@byline/ui/react'
|
|
25
25
|
import cx from 'clsx'
|
|
26
26
|
|
|
27
27
|
import { sliceFieldAdmin } from '../fields/field-admin'
|
|
@@ -263,6 +263,10 @@ const FormContent = ({
|
|
|
263
263
|
const [isUploading, setIsUploading] = useState(false)
|
|
264
264
|
const submittingRef = useRef(false)
|
|
265
265
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
266
|
+
const isBusy = isUploading || isSubmitting
|
|
267
|
+
const formRef = useRef<HTMLFormElement>(null)
|
|
268
|
+
const focusBeforeBusyRef = useRef<HTMLElement | null>(null)
|
|
269
|
+
const restoreFocusAfterBusyRef = useRef(false)
|
|
266
270
|
// Block-only "save first" guard. Set true when the editor triggers a
|
|
267
271
|
// guarded action (status change, duplicate, copy-to-locale) while the form
|
|
268
272
|
// is dirty — those actions operate on the saved version, so unsaved edits
|
|
@@ -392,6 +396,34 @@ const FormContent = ({
|
|
|
392
396
|
return subscribeMeta(() => setFormData(getFieldValues()))
|
|
393
397
|
}, [subscribeMeta, getFieldValues])
|
|
394
398
|
|
|
399
|
+
// `inert` removes the active control from the tab order while a save is in
|
|
400
|
+
// flight. Restore the editor's position after React has removed `inert`;
|
|
401
|
+
// when the original control became disabled, fall back to the first usable
|
|
402
|
+
// form control instead of leaving focus on <body>.
|
|
403
|
+
useEffect(() => {
|
|
404
|
+
if (isBusy || !restoreFocusAfterBusyRef.current) return
|
|
405
|
+
restoreFocusAfterBusyRef.current = false
|
|
406
|
+
|
|
407
|
+
const original = focusBeforeBusyRef.current
|
|
408
|
+
focusBeforeBusyRef.current = null
|
|
409
|
+
const originalCanReceiveFocus =
|
|
410
|
+
original?.isConnected === true && !original.matches(':disabled, [aria-disabled="true"]')
|
|
411
|
+
const target = originalCanReceiveFocus
|
|
412
|
+
? original
|
|
413
|
+
: formRef.current?.querySelector<HTMLElement>(
|
|
414
|
+
'input:not(:disabled), textarea:not(:disabled), select:not(:disabled), button:not(:disabled), [tabindex]:not([tabindex="-1"])'
|
|
415
|
+
)
|
|
416
|
+
target?.focus({ preventScroll: true })
|
|
417
|
+
}, [isBusy])
|
|
418
|
+
|
|
419
|
+
const captureFocusBeforeBusy = useCallback(() => {
|
|
420
|
+
if (focusBeforeBusyRef.current != null) return
|
|
421
|
+
const activeElement = document.activeElement
|
|
422
|
+
if (!(activeElement instanceof HTMLElement) || !formRef.current?.contains(activeElement)) return
|
|
423
|
+
focusBeforeBusyRef.current = activeElement
|
|
424
|
+
restoreFocusAfterBusyRef.current = true
|
|
425
|
+
}, [])
|
|
426
|
+
|
|
395
427
|
const handleCancel = () => {
|
|
396
428
|
if (onCancel && typeof onCancel === 'function') {
|
|
397
429
|
onCancel()
|
|
@@ -409,6 +441,7 @@ const FormContent = ({
|
|
|
409
441
|
if (typeof onSubmit !== 'function') return
|
|
410
442
|
if (submittingRef.current) return
|
|
411
443
|
submittingRef.current = true
|
|
444
|
+
captureFocusBeforeBusy()
|
|
412
445
|
setIsSubmitting(true)
|
|
413
446
|
try {
|
|
414
447
|
await onSubmit(payload)
|
|
@@ -421,7 +454,7 @@ const FormContent = ({
|
|
|
421
454
|
setIsSubmitting(false)
|
|
422
455
|
}
|
|
423
456
|
},
|
|
424
|
-
[onSubmit, resetHasChanges]
|
|
457
|
+
[captureFocusBeforeBusy, onSubmit, resetHasChanges]
|
|
425
458
|
)
|
|
426
459
|
|
|
427
460
|
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
|
@@ -441,6 +474,7 @@ const FormContent = ({
|
|
|
441
474
|
// Execute any pending uploads before submitting
|
|
442
475
|
const pendingUploads = getPendingUploads()
|
|
443
476
|
if (pendingUploads.size > 0) {
|
|
477
|
+
captureFocusBeforeBusy()
|
|
444
478
|
setIsUploading(true)
|
|
445
479
|
try {
|
|
446
480
|
const uploadResult = await executeUploadsWithProgress(
|
|
@@ -605,217 +639,261 @@ const FormContent = ({
|
|
|
605
639
|
)
|
|
606
640
|
}
|
|
607
641
|
|
|
642
|
+
const busyAnnouncement = isUploading
|
|
643
|
+
? t('forms.actions.uploading')
|
|
644
|
+
: isSubmitting
|
|
645
|
+
? t('forms.actions.saving')
|
|
646
|
+
: ''
|
|
647
|
+
|
|
608
648
|
return (
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
<div
|
|
618
|
-
<
|
|
619
|
-
|
|
649
|
+
<>
|
|
650
|
+
<span
|
|
651
|
+
className={cx('byline-form-busy-status', styles['busy-status'])}
|
|
652
|
+
role="status"
|
|
653
|
+
aria-live="polite"
|
|
654
|
+
>
|
|
655
|
+
{busyAnnouncement}
|
|
656
|
+
</span>
|
|
657
|
+
<div aria-busy={isBusy}>
|
|
658
|
+
<form
|
|
659
|
+
ref={formRef}
|
|
660
|
+
method="post"
|
|
661
|
+
noValidate
|
|
662
|
+
onSubmit={handleSubmit}
|
|
663
|
+
className={cx('byline-form', styles.form)}
|
|
664
|
+
inert={isBusy ? true : undefined}
|
|
665
|
+
>
|
|
666
|
+
<div className={cx('byline-form-heading-row', styles['heading-row'])}>
|
|
667
|
+
<h1 className={cx('byline-form-heading', styles.heading)}>{computedHeading}</h1>
|
|
668
|
+
{/* Source-locale anchor indicator removed pending heading-layout work.
|
|
620
669
|
To re-enable: render `<SourceLocaleBadge locale={sourceLocale} />`
|
|
621
670
|
here from `initialData.sourceLocale` (mismatch-only is the intended
|
|
622
671
|
end state). See docs/08-internationalization/index.md. */}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
onClick={handleCancel}
|
|
644
|
-
>
|
|
645
|
-
{hasChanges === false ? t('common.actions.close') : t('common.actions.cancel')}
|
|
646
|
-
</Button>
|
|
647
|
-
<Button
|
|
648
|
-
className={cx('byline-form-actions-button', styles['actions-button'])}
|
|
649
|
-
size="sm"
|
|
650
|
-
type="submit"
|
|
651
|
-
disabled={hasChanges === false || isUploading || isSubmitting}
|
|
652
|
-
>
|
|
653
|
-
{isUploading ? t('forms.actions.uploading') : t('common.actions.save')}
|
|
654
|
-
</Button>
|
|
655
|
-
{primaryStatus && onStatusChange && (
|
|
656
|
-
<div className={cx('byline-form-actions-status-wrap', styles['actions-status-wrap'])}>
|
|
657
|
-
<ComboButton
|
|
658
|
-
buttonClassName={cx(
|
|
659
|
-
'byline-form-actions-combo-button',
|
|
660
|
-
styles['actions-combo-button']
|
|
661
|
-
)}
|
|
662
|
-
triggerClassName={cx(
|
|
663
|
-
'byline-form-actions-combo-trigger',
|
|
664
|
-
styles['actions-combo-trigger']
|
|
665
|
-
)}
|
|
666
|
-
options={secondaryStatuses.map((s) => ({
|
|
667
|
-
label: isTerminal
|
|
668
|
-
? t('forms.actions.revertTo', { label: s.label ?? s.name })
|
|
669
|
-
: (s.verb ?? s.label ?? s.name),
|
|
670
|
-
value: s.name,
|
|
671
|
-
}))}
|
|
672
|
-
sideOffset={5}
|
|
672
|
+
{headerSlot}
|
|
673
|
+
</div>
|
|
674
|
+
<div className={cx('byline-form-status-bar', styles['status-bar'])}>
|
|
675
|
+
<div className={cx('byline-form-status-details', styles['status-details'])}>
|
|
676
|
+
<FormStatusDisplay
|
|
677
|
+
initialData={initialData}
|
|
678
|
+
workflowStatuses={workflowStatuses}
|
|
679
|
+
publishedVersion={publishedVersion}
|
|
680
|
+
onUnpublish={onUnpublish}
|
|
681
|
+
afterStatusCells={
|
|
682
|
+
<ScheduledPublicationCell
|
|
683
|
+
state={scheduling.state}
|
|
684
|
+
timeZone={scheduling.timeZone}
|
|
685
|
+
/>
|
|
686
|
+
}
|
|
687
|
+
/>
|
|
688
|
+
</div>
|
|
689
|
+
<div className={cx('byline-form-actions', styles.actions)}>
|
|
690
|
+
<Button
|
|
691
|
+
className={cx('byline-form-actions-button', styles['actions-button'])}
|
|
673
692
|
size="sm"
|
|
693
|
+
intent="noeffect"
|
|
674
694
|
type="button"
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
695
|
+
onClick={handleCancel}
|
|
696
|
+
>
|
|
697
|
+
{hasChanges === false ? t('common.actions.close') : t('common.actions.cancel')}
|
|
698
|
+
</Button>
|
|
699
|
+
<Button
|
|
700
|
+
className={cx('byline-form-actions-button', styles['actions-button'])}
|
|
701
|
+
size="sm"
|
|
702
|
+
type="submit"
|
|
703
|
+
disabled={hasChanges === false || isUploading || isSubmitting}
|
|
704
|
+
aria-label={isSubmitting ? t('common.actions.save') : undefined}
|
|
705
|
+
>
|
|
706
|
+
{isUploading ? (
|
|
707
|
+
t('forms.actions.uploading')
|
|
708
|
+
) : (
|
|
709
|
+
<span className={cx('byline-form-save-content', styles['save-content'])}>
|
|
710
|
+
<span
|
|
711
|
+
className={cx(
|
|
712
|
+
'byline-form-save-label',
|
|
713
|
+
styles['save-label'],
|
|
714
|
+
isSubmitting && styles['save-label-hidden']
|
|
715
|
+
)}
|
|
716
|
+
>
|
|
717
|
+
{t('common.actions.save')}
|
|
718
|
+
</span>
|
|
719
|
+
{isSubmitting ? (
|
|
720
|
+
<span className={cx('byline-form-save-loader', styles['save-loader'])}>
|
|
721
|
+
<LoaderEllipsis size={28} aria-hidden="true" />
|
|
722
|
+
</span>
|
|
723
|
+
) : null}
|
|
724
|
+
</span>
|
|
725
|
+
)}
|
|
726
|
+
</Button>
|
|
727
|
+
{primaryStatus && onStatusChange && (
|
|
728
|
+
<div
|
|
729
|
+
className={cx('byline-form-actions-status-wrap', styles['actions-status-wrap'])}
|
|
730
|
+
>
|
|
731
|
+
<ComboButton
|
|
732
|
+
buttonClassName={cx(
|
|
733
|
+
'byline-form-actions-combo-button',
|
|
734
|
+
styles['actions-combo-button']
|
|
735
|
+
)}
|
|
736
|
+
triggerClassName={cx(
|
|
737
|
+
'byline-form-actions-combo-trigger',
|
|
738
|
+
styles['actions-combo-trigger']
|
|
739
|
+
)}
|
|
740
|
+
options={secondaryStatuses.map((s) => ({
|
|
741
|
+
label: isTerminal
|
|
742
|
+
? t('forms.actions.revertTo', { label: s.label ?? s.name })
|
|
743
|
+
: (s.verb ?? s.label ?? s.name),
|
|
744
|
+
value: s.name,
|
|
745
|
+
}))}
|
|
746
|
+
sideOffset={5}
|
|
747
|
+
size="sm"
|
|
748
|
+
type="button"
|
|
749
|
+
intent={isTerminal ? 'info' : 'success'}
|
|
750
|
+
disabled={statusBusy}
|
|
751
|
+
onOptionSelect={async (value: string) => {
|
|
752
|
+
if (hasChanges) {
|
|
753
|
+
setShowUnsavedModal(true)
|
|
754
|
+
return
|
|
703
755
|
}
|
|
756
|
+
setStatusBusy(true)
|
|
757
|
+
try {
|
|
758
|
+
await onStatusChange(value)
|
|
759
|
+
} finally {
|
|
760
|
+
setStatusBusy(false)
|
|
761
|
+
}
|
|
762
|
+
}}
|
|
763
|
+
onButtonClick={
|
|
764
|
+
isTerminal
|
|
765
|
+
? undefined
|
|
766
|
+
: async () => {
|
|
767
|
+
if (hasChanges) {
|
|
768
|
+
setShowUnsavedModal(true)
|
|
769
|
+
return
|
|
770
|
+
}
|
|
771
|
+
setStatusBusy(true)
|
|
772
|
+
try {
|
|
773
|
+
await onStatusChange(primaryStatus.name)
|
|
774
|
+
} finally {
|
|
775
|
+
setStatusBusy(false)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
>
|
|
780
|
+
{statusBusy
|
|
781
|
+
? '...'
|
|
782
|
+
: isTerminal
|
|
783
|
+
? (primaryStatus.label ?? primaryStatus.name)
|
|
784
|
+
: (primaryStatus.verb ?? primaryStatus.label ?? primaryStatus.name)}
|
|
785
|
+
</ComboButton>
|
|
786
|
+
</div>
|
|
787
|
+
)}
|
|
788
|
+
<DocumentActions
|
|
789
|
+
publishedVersion={publishedVersion}
|
|
790
|
+
onUnpublish={onUnpublish}
|
|
791
|
+
onDelete={onDelete}
|
|
792
|
+
onDuplicate={onDuplicate}
|
|
793
|
+
sourceTitle={
|
|
794
|
+
useAsTitle != null && initialData != null
|
|
795
|
+
? ((initialData as Record<string, unknown>)[useAsTitle] as
|
|
796
|
+
| string
|
|
797
|
+
| null
|
|
798
|
+
| undefined)
|
|
799
|
+
: null
|
|
704
800
|
}
|
|
705
|
-
|
|
706
|
-
{
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
</ComboButton>
|
|
712
|
-
</div>
|
|
713
|
-
)}
|
|
714
|
-
<DocumentActions
|
|
715
|
-
publishedVersion={publishedVersion}
|
|
716
|
-
onUnpublish={onUnpublish}
|
|
717
|
-
onDelete={onDelete}
|
|
718
|
-
onDuplicate={onDuplicate}
|
|
719
|
-
sourceTitle={
|
|
720
|
-
useAsTitle != null && initialData != null
|
|
721
|
-
? ((initialData as Record<string, unknown>)[useAsTitle] as
|
|
722
|
-
| string
|
|
723
|
-
| null
|
|
724
|
-
| undefined)
|
|
725
|
-
: null
|
|
726
|
-
}
|
|
727
|
-
onCopyToLocale={onCopyToLocale}
|
|
728
|
-
sourceLocale={contentLocale}
|
|
729
|
-
contentLocales={contentLocales}
|
|
730
|
-
hasUnsavedChanges={hasChanges}
|
|
731
|
-
onUnsavedChanges={() => setShowUnsavedModal(true)}
|
|
732
|
-
onDeleteLocale={onDeleteLocale}
|
|
733
|
-
defaultLocale={defaultLocale}
|
|
734
|
-
availableLocales={initialData?._availableVersionLocales as string[] | undefined}
|
|
735
|
-
scheduledPublicationState={scheduling.state}
|
|
736
|
-
onSchedulePublication={scheduling.openSchedule}
|
|
737
|
-
onConfirmScheduledPublication={scheduling.confirm}
|
|
738
|
-
onCancelScheduledPublication={scheduling.cancel}
|
|
739
|
-
/>
|
|
740
|
-
</div>
|
|
741
|
-
</div>
|
|
742
|
-
<ScheduledPublicationNotice
|
|
743
|
-
state={scheduling.state}
|
|
744
|
-
timeZone={scheduling.timeZone}
|
|
745
|
-
busy={scheduling.busy}
|
|
746
|
-
onConfirm={scheduling.confirm}
|
|
747
|
-
onReschedule={scheduling.openSchedule}
|
|
748
|
-
onCancel={scheduling.cancel}
|
|
749
|
-
/>
|
|
750
|
-
{scheduling.modal}
|
|
751
|
-
{restoreWarnings && restoreWarnings.length > 0 && (
|
|
752
|
-
<Alert
|
|
753
|
-
className="m-0 mt-4"
|
|
754
|
-
intent="warning"
|
|
755
|
-
icon={true}
|
|
756
|
-
close={false}
|
|
757
|
-
title={t('forms.restoreWarnings.title')}
|
|
758
|
-
>
|
|
759
|
-
<p>{t('forms.restoreWarnings.body', { count: restoreWarnings.length })}</p>
|
|
760
|
-
<ul>
|
|
761
|
-
{restoreWarnings.map((w) => (
|
|
762
|
-
<li key={w}>{w}</li>
|
|
763
|
-
))}
|
|
764
|
-
</ul>
|
|
765
|
-
</Alert>
|
|
766
|
-
)}
|
|
767
|
-
<div className={cx('byline-form-layout', styles.layout)}>
|
|
768
|
-
<div className={cx('byline-form-content', styles.content)}>
|
|
769
|
-
{layout.main.map((name) => renderItem(name))}
|
|
770
|
-
</div>
|
|
771
|
-
<div className={cx('byline-form-sidebar', styles.sidebar)}>
|
|
772
|
-
{showPath &&
|
|
773
|
-
(useAsPath ||
|
|
774
|
-
(typeof initialData?.path === 'string' && initialData.path.length > 0)) && (
|
|
775
|
-
<PathWidget
|
|
776
|
-
useAsPath={useAsPath}
|
|
777
|
-
collectionPath={collectionPath ?? ''}
|
|
801
|
+
onCopyToLocale={onCopyToLocale}
|
|
802
|
+
sourceLocale={contentLocale}
|
|
803
|
+
contentLocales={contentLocales}
|
|
804
|
+
hasUnsavedChanges={hasChanges}
|
|
805
|
+
onUnsavedChanges={() => setShowUnsavedModal(true)}
|
|
806
|
+
onDeleteLocale={onDeleteLocale}
|
|
778
807
|
defaultLocale={defaultLocale}
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
808
|
+
availableLocales={initialData?._availableVersionLocales as string[] | undefined}
|
|
809
|
+
scheduledPublicationState={scheduling.state}
|
|
810
|
+
onSchedulePublication={scheduling.openSchedule}
|
|
811
|
+
onConfirmScheduledPublication={scheduling.confirm}
|
|
812
|
+
onCancelScheduledPublication={scheduling.cancel}
|
|
783
813
|
/>
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
<ScheduledPublicationNotice
|
|
817
|
+
state={scheduling.state}
|
|
818
|
+
timeZone={scheduling.timeZone}
|
|
819
|
+
busy={scheduling.busy}
|
|
820
|
+
onConfirm={scheduling.confirm}
|
|
821
|
+
onReschedule={scheduling.openSchedule}
|
|
822
|
+
onCancel={scheduling.cancel}
|
|
823
|
+
/>
|
|
824
|
+
{scheduling.modal}
|
|
825
|
+
{restoreWarnings && restoreWarnings.length > 0 && (
|
|
826
|
+
<Alert
|
|
827
|
+
className="m-0 mt-4"
|
|
828
|
+
intent="warning"
|
|
829
|
+
icon={true}
|
|
830
|
+
close={false}
|
|
831
|
+
title={t('forms.restoreWarnings.title')}
|
|
832
|
+
>
|
|
833
|
+
<p>{t('forms.restoreWarnings.body', { count: restoreWarnings.length })}</p>
|
|
834
|
+
<ul>
|
|
835
|
+
{restoreWarnings.map((w) => (
|
|
836
|
+
<li key={w}>{w}</li>
|
|
837
|
+
))}
|
|
838
|
+
</ul>
|
|
839
|
+
</Alert>
|
|
791
840
|
)}
|
|
792
|
-
{
|
|
793
|
-
<
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
841
|
+
<div className={cx('byline-form-layout', styles.layout)}>
|
|
842
|
+
<div className={cx('byline-form-content', styles.content)}>
|
|
843
|
+
{layout.main.map((name) => renderItem(name))}
|
|
844
|
+
</div>
|
|
845
|
+
<div className={cx('byline-form-sidebar', styles.sidebar)}>
|
|
846
|
+
{showPath &&
|
|
847
|
+
(useAsPath ||
|
|
848
|
+
(typeof initialData?.path === 'string' && initialData.path.length > 0)) && (
|
|
849
|
+
<PathWidget
|
|
850
|
+
useAsPath={useAsPath}
|
|
851
|
+
collectionPath={collectionPath ?? ''}
|
|
852
|
+
defaultLocale={defaultLocale}
|
|
853
|
+
activeLocale={contentLocale}
|
|
854
|
+
mode={mode}
|
|
855
|
+
slugifier={pathSlugifier}
|
|
856
|
+
sourceLocked={pathSourceLocked}
|
|
857
|
+
/>
|
|
858
|
+
)}
|
|
859
|
+
{tree && mode === 'edit' && typeof initialData?.id === 'string' && (
|
|
860
|
+
<TreePlacementWidget
|
|
861
|
+
collectionPath={collectionPath ?? ''}
|
|
862
|
+
documentId={initialData.id as string}
|
|
863
|
+
useAsTitle={useAsTitle}
|
|
864
|
+
/>
|
|
865
|
+
)}
|
|
866
|
+
{advertiseLocales && (
|
|
867
|
+
<AvailableLocalesWidget
|
|
868
|
+
contentLocales={contentLocales ?? []}
|
|
869
|
+
availableVersionLocales={
|
|
870
|
+
(initialData?._availableVersionLocales as string[] | undefined) ?? []
|
|
871
|
+
}
|
|
872
|
+
/>
|
|
873
|
+
)}
|
|
874
|
+
{(layout.sidebar ?? []).map((name) => renderItem(name))}
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
{showUnsavedModal && <UnsavedChangesModal onClose={() => setShowUnsavedModal(false)} />}
|
|
878
|
+
{pendingSystemFieldsSubmit != null && (
|
|
879
|
+
<SystemFieldsConfirmModal
|
|
880
|
+
contentDirty={pendingSystemFieldsSubmit.contentDirty}
|
|
881
|
+
pathDirty={pendingSystemFieldsSubmit.pathDirty}
|
|
882
|
+
availableLocalesDirty={pendingSystemFieldsSubmit.availableLocalesDirty}
|
|
883
|
+
onCancel={() => setPendingSystemFieldsSubmit(null)}
|
|
884
|
+
onConfirm={() => {
|
|
885
|
+
const payload = pendingSystemFieldsSubmit
|
|
886
|
+
setPendingSystemFieldsSubmit(null)
|
|
887
|
+
void submitPayload(payload)
|
|
888
|
+
}}
|
|
798
889
|
/>
|
|
799
890
|
)}
|
|
800
|
-
{
|
|
801
|
-
|
|
891
|
+
{guard.isBlocked && (
|
|
892
|
+
<NavigationGuardModal onStay={guard.stay} onProceed={guard.proceed} />
|
|
893
|
+
)}
|
|
894
|
+
</form>
|
|
802
895
|
</div>
|
|
803
|
-
|
|
804
|
-
{pendingSystemFieldsSubmit != null && (
|
|
805
|
-
<SystemFieldsConfirmModal
|
|
806
|
-
contentDirty={pendingSystemFieldsSubmit.contentDirty}
|
|
807
|
-
pathDirty={pendingSystemFieldsSubmit.pathDirty}
|
|
808
|
-
availableLocalesDirty={pendingSystemFieldsSubmit.availableLocalesDirty}
|
|
809
|
-
onCancel={() => setPendingSystemFieldsSubmit(null)}
|
|
810
|
-
onConfirm={() => {
|
|
811
|
-
const payload = pendingSystemFieldsSubmit
|
|
812
|
-
setPendingSystemFieldsSubmit(null)
|
|
813
|
-
void submitPayload(payload)
|
|
814
|
-
}}
|
|
815
|
-
/>
|
|
816
|
-
)}
|
|
817
|
-
{guard.isBlocked && <NavigationGuardModal onStay={guard.stay} onProceed={guard.proceed} />}
|
|
818
|
-
</form>
|
|
896
|
+
</>
|
|
819
897
|
)
|
|
820
898
|
}
|
|
821
899
|
|