@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.
- package/dist/fields/field-services-types.d.ts +5 -2
- package/dist/forms/available-locales-widget.d.ts +2 -1
- package/dist/forms/available-locales-widget.js +4 -2
- package/dist/forms/document-actions.d.ts +2 -1
- package/dist/forms/document-actions.js +32 -14
- package/dist/forms/form-renderer.d.ts +9 -0
- package/dist/forms/form-renderer.js +338 -191
- package/dist/forms/form-renderer.module.js +11 -0
- package/dist/forms/form-renderer_module.css +46 -0
- package/dist/forms/form-status-display.d.ts +2 -1
- package/dist/forms/form-status-display.js +5 -2
- package/dist/forms/path-widget.d.ts +2 -1
- package/dist/forms/path-widget.js +7 -2
- package/dist/forms/scheduled-publication-control.d.ts +2 -1
- package/dist/forms/scheduled-publication-control.js +12 -8
- package/dist/forms/tree-placement-widget.d.ts +5 -1
- package/dist/forms/tree-placement-widget.js +14 -7
- package/package.json +7 -7
- package/src/fields/field-services-types.ts +10 -2
- package/src/forms/available-locales-widget.tsx +5 -2
- package/src/forms/document-actions.tsx +55 -20
- package/src/forms/form-renderer-submit.test.tsx +231 -8
- package/src/forms/form-renderer.module.css +62 -0
- package/src/forms/form-renderer.tsx +393 -200
- package/src/forms/form-status-display.tsx +6 -1
- package/src/forms/path-widget.tsx +8 -3
- package/src/forms/scheduled-publication-control.tsx +16 -7
- package/src/forms/tree-placement-widget.tsx +34 -7
|
@@ -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'
|
|
@@ -80,6 +80,15 @@ export interface SystemFieldsSubmitPayload {
|
|
|
80
80
|
|
|
81
81
|
/** Props shared by both the public FormRenderer and its internal FormContent component. */
|
|
82
82
|
export interface FormRendererProps {
|
|
83
|
+
mutationIssue?: 'stale' | 'reload' | 'lock' | 'unavailable' | 'committed' | null
|
|
84
|
+
mutationsBlocked?: boolean
|
|
85
|
+
observedRevision?: number
|
|
86
|
+
onMutationError?: (error: unknown) => 'blocked' | 'committed' | null | void
|
|
87
|
+
onTreeMutationCommitted?: (receipt: import('@byline/core').StructuralMutationReceipt) => void
|
|
88
|
+
scheduledPublicationsNeedReconfirmation?: boolean
|
|
89
|
+
scheduledPublicationsHref?: string
|
|
90
|
+
/** Explicit discard action; defaults to a complete server reload. */
|
|
91
|
+
onReloadDocument?: () => void | Promise<void>
|
|
83
92
|
mode: 'create' | 'edit'
|
|
84
93
|
fields: Field[]
|
|
85
94
|
onSubmit: (data: any) => void | Promise<void>
|
|
@@ -196,6 +205,14 @@ export interface FormRendererProps {
|
|
|
196
205
|
|
|
197
206
|
const FormContent = ({
|
|
198
207
|
mode,
|
|
208
|
+
mutationIssue,
|
|
209
|
+
mutationsBlocked = false,
|
|
210
|
+
observedRevision,
|
|
211
|
+
onMutationError,
|
|
212
|
+
onTreeMutationCommitted,
|
|
213
|
+
scheduledPublicationsNeedReconfirmation = false,
|
|
214
|
+
scheduledPublicationsHref,
|
|
215
|
+
onReloadDocument,
|
|
199
216
|
fields,
|
|
200
217
|
onSubmit,
|
|
201
218
|
onCancel,
|
|
@@ -263,6 +280,10 @@ const FormContent = ({
|
|
|
263
280
|
const [isUploading, setIsUploading] = useState(false)
|
|
264
281
|
const submittingRef = useRef(false)
|
|
265
282
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
283
|
+
const isBusy = isUploading || isSubmitting
|
|
284
|
+
const formRef = useRef<HTMLFormElement>(null)
|
|
285
|
+
const focusBeforeBusyRef = useRef<HTMLElement | null>(null)
|
|
286
|
+
const restoreFocusAfterBusyRef = useRef(false)
|
|
266
287
|
// Block-only "save first" guard. Set true when the editor triggers a
|
|
267
288
|
// guarded action (status change, duplicate, copy-to-locale) while the form
|
|
268
289
|
// is dirty — those actions operate on the saved version, so unsaved edits
|
|
@@ -279,6 +300,7 @@ const FormContent = ({
|
|
|
279
300
|
// escalated notice, and the schedule modal — so its state lives here and the
|
|
280
301
|
// surfaces are rendered where each belongs.
|
|
281
302
|
const scheduling = useScheduledPublication({
|
|
303
|
+
disabled: mutationsBlocked,
|
|
282
304
|
schedule: scheduledPublication ?? null,
|
|
283
305
|
onSchedule: onSchedulePublication,
|
|
284
306
|
onConfirm: onConfirmScheduledPublication,
|
|
@@ -369,7 +391,25 @@ const FormContent = ({
|
|
|
369
391
|
// The guard hook is injected by the consuming framework (prop > context > no-op fallback).
|
|
370
392
|
const guardFromContext = useNavigationGuardAdapter()
|
|
371
393
|
const useGuard = useNavigationGuardProp ?? guardFromContext
|
|
372
|
-
const
|
|
394
|
+
const [discarding, setDiscarding] = useState(false)
|
|
395
|
+
const [reloadFailed, setReloadFailed] = useState(false)
|
|
396
|
+
const warningRef = useRef<HTMLDivElement>(null)
|
|
397
|
+
const mutationBlockedRef = useRef(mutationsBlocked)
|
|
398
|
+
mutationBlockedRef.current = mutationsBlocked || discarding
|
|
399
|
+
const guard = useGuard(hasChanges && !discarding)
|
|
400
|
+
useEffect(() => {
|
|
401
|
+
if (mutationIssue) warningRef.current?.focus()
|
|
402
|
+
}, [mutationIssue])
|
|
403
|
+
useEffect(() => {
|
|
404
|
+
if (!discarding) return
|
|
405
|
+
// Let the guard's beforeunload listener detach before the explicit discard.
|
|
406
|
+
Promise.resolve()
|
|
407
|
+
.then(() => (onReloadDocument ? onReloadDocument() : window.location.reload()))
|
|
408
|
+
.catch(() => {
|
|
409
|
+
setDiscarding(false)
|
|
410
|
+
setReloadFailed(true)
|
|
411
|
+
})
|
|
412
|
+
}, [discarding, onReloadDocument])
|
|
373
413
|
|
|
374
414
|
// Compute available status transitions
|
|
375
415
|
const currentStatus = initialData?.status
|
|
@@ -392,6 +432,39 @@ const FormContent = ({
|
|
|
392
432
|
return subscribeMeta(() => setFormData(getFieldValues()))
|
|
393
433
|
}, [subscribeMeta, getFieldValues])
|
|
394
434
|
|
|
435
|
+
// `inert` removes the active control from the tab order while a save is in
|
|
436
|
+
// flight. Restore the editor's position after React has removed `inert`;
|
|
437
|
+
// when the original control became disabled, fall back to the first usable
|
|
438
|
+
// form control instead of leaving focus on <body>.
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
if (isBusy || !restoreFocusAfterBusyRef.current) return
|
|
441
|
+
restoreFocusAfterBusyRef.current = false
|
|
442
|
+
|
|
443
|
+
if (mutationIssue) {
|
|
444
|
+
warningRef.current?.focus()
|
|
445
|
+
focusBeforeBusyRef.current = null
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
const original = focusBeforeBusyRef.current
|
|
449
|
+
focusBeforeBusyRef.current = null
|
|
450
|
+
const originalCanReceiveFocus =
|
|
451
|
+
original?.isConnected === true && !original.matches(':disabled, [aria-disabled="true"]')
|
|
452
|
+
const target = originalCanReceiveFocus
|
|
453
|
+
? original
|
|
454
|
+
: formRef.current?.querySelector<HTMLElement>(
|
|
455
|
+
'input:not(:disabled), textarea:not(:disabled), select:not(:disabled), button:not(:disabled), [tabindex]:not([tabindex="-1"])'
|
|
456
|
+
)
|
|
457
|
+
target?.focus({ preventScroll: true })
|
|
458
|
+
}, [isBusy, mutationIssue])
|
|
459
|
+
|
|
460
|
+
const captureFocusBeforeBusy = useCallback(() => {
|
|
461
|
+
if (focusBeforeBusyRef.current != null) return
|
|
462
|
+
const activeElement = document.activeElement
|
|
463
|
+
if (!(activeElement instanceof HTMLElement) || !formRef.current?.contains(activeElement)) return
|
|
464
|
+
focusBeforeBusyRef.current = activeElement
|
|
465
|
+
restoreFocusAfterBusyRef.current = true
|
|
466
|
+
}, [])
|
|
467
|
+
|
|
395
468
|
const handleCancel = () => {
|
|
396
469
|
if (onCancel && typeof onCancel === 'function') {
|
|
397
470
|
onCancel()
|
|
@@ -406,9 +479,10 @@ const FormContent = ({
|
|
|
406
479
|
// mid-flight; the mirrored state drives the Save button's disabled prop.
|
|
407
480
|
const submitPayload = useCallback(
|
|
408
481
|
async (payload: SystemFieldsSubmitPayload) => {
|
|
409
|
-
if (typeof onSubmit !== 'function') return
|
|
482
|
+
if (mutationBlockedRef.current || typeof onSubmit !== 'function') return
|
|
410
483
|
if (submittingRef.current) return
|
|
411
484
|
submittingRef.current = true
|
|
485
|
+
captureFocusBeforeBusy()
|
|
412
486
|
setIsSubmitting(true)
|
|
413
487
|
try {
|
|
414
488
|
await onSubmit(payload)
|
|
@@ -421,10 +495,14 @@ const FormContent = ({
|
|
|
421
495
|
setIsSubmitting(false)
|
|
422
496
|
}
|
|
423
497
|
},
|
|
424
|
-
[onSubmit, resetHasChanges]
|
|
498
|
+
[captureFocusBeforeBusy, onSubmit, resetHasChanges]
|
|
425
499
|
)
|
|
426
500
|
|
|
427
501
|
const handleSubmit = (e: React.SubmitEvent<HTMLFormElement>) => {
|
|
502
|
+
if (mutationBlockedRef.current) {
|
|
503
|
+
e.preventDefault()
|
|
504
|
+
return
|
|
505
|
+
}
|
|
428
506
|
e.preventDefault()
|
|
429
507
|
|
|
430
508
|
// Run field-level beforeValidate hooks (submit-time), then validate
|
|
@@ -438,9 +516,12 @@ const FormContent = ({
|
|
|
438
516
|
return
|
|
439
517
|
}
|
|
440
518
|
|
|
519
|
+
if (mutationBlockedRef.current) return
|
|
520
|
+
|
|
441
521
|
// Execute any pending uploads before submitting
|
|
442
522
|
const pendingUploads = getPendingUploads()
|
|
443
523
|
if (pendingUploads.size > 0) {
|
|
524
|
+
captureFocusBeforeBusy()
|
|
444
525
|
setIsUploading(true)
|
|
445
526
|
try {
|
|
446
527
|
const uploadResult = await executeUploadsWithProgress(
|
|
@@ -605,217 +686,329 @@ const FormContent = ({
|
|
|
605
686
|
)
|
|
606
687
|
}
|
|
607
688
|
|
|
689
|
+
const busyAnnouncement = isUploading
|
|
690
|
+
? t('forms.actions.uploading')
|
|
691
|
+
: isSubmitting
|
|
692
|
+
? t('forms.actions.saving')
|
|
693
|
+
: ''
|
|
694
|
+
|
|
608
695
|
return (
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
<div
|
|
618
|
-
<
|
|
619
|
-
|
|
696
|
+
<>
|
|
697
|
+
<span
|
|
698
|
+
className={cx('byline-form-busy-status', styles['busy-status'])}
|
|
699
|
+
role="status"
|
|
700
|
+
aria-live="polite"
|
|
701
|
+
>
|
|
702
|
+
{busyAnnouncement}
|
|
703
|
+
</span>
|
|
704
|
+
<div aria-busy={isBusy}>
|
|
705
|
+
<form
|
|
706
|
+
ref={formRef}
|
|
707
|
+
method="post"
|
|
708
|
+
noValidate
|
|
709
|
+
onSubmit={handleSubmit}
|
|
710
|
+
className={cx('byline-form', styles.form)}
|
|
711
|
+
inert={isBusy ? true : undefined}
|
|
712
|
+
>
|
|
713
|
+
<div className={cx('byline-form-heading-row', styles['heading-row'])}>
|
|
714
|
+
<h1 className={cx('byline-form-heading', styles.heading)}>{computedHeading}</h1>
|
|
715
|
+
{/* Source-locale anchor indicator removed pending heading-layout work.
|
|
620
716
|
To re-enable: render `<SourceLocaleBadge locale={sourceLocale} />`
|
|
621
717
|
here from `initialData.sourceLocale` (mismatch-only is the intended
|
|
622
718
|
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
|
-
|
|
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}
|
|
719
|
+
{headerSlot}
|
|
720
|
+
</div>
|
|
721
|
+
<div className={cx('byline-form-status-bar', styles['status-bar'])}>
|
|
722
|
+
<div className={cx('byline-form-status-details', styles['status-details'])}>
|
|
723
|
+
<FormStatusDisplay
|
|
724
|
+
disabled={mutationsBlocked || discarding}
|
|
725
|
+
initialData={initialData}
|
|
726
|
+
workflowStatuses={workflowStatuses}
|
|
727
|
+
publishedVersion={publishedVersion}
|
|
728
|
+
onUnpublish={onUnpublish}
|
|
729
|
+
afterStatusCells={
|
|
730
|
+
<ScheduledPublicationCell
|
|
731
|
+
state={scheduling.state}
|
|
732
|
+
timeZone={scheduling.timeZone}
|
|
733
|
+
/>
|
|
734
|
+
}
|
|
735
|
+
/>
|
|
736
|
+
</div>
|
|
737
|
+
<div className={cx('byline-form-actions', styles.actions)}>
|
|
738
|
+
<Button
|
|
739
|
+
className={cx('byline-form-actions-button', styles['actions-button'])}
|
|
673
740
|
size="sm"
|
|
741
|
+
intent="noeffect"
|
|
674
742
|
type="button"
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
onButtonClick={
|
|
690
|
-
isTerminal
|
|
691
|
-
? undefined
|
|
692
|
-
: async () => {
|
|
693
|
-
if (hasChanges) {
|
|
694
|
-
setShowUnsavedModal(true)
|
|
695
|
-
return
|
|
696
|
-
}
|
|
697
|
-
setStatusBusy(true)
|
|
698
|
-
try {
|
|
699
|
-
await onStatusChange(primaryStatus.name)
|
|
700
|
-
} finally {
|
|
701
|
-
setStatusBusy(false)
|
|
702
|
-
}
|
|
703
|
-
}
|
|
743
|
+
onClick={handleCancel}
|
|
744
|
+
>
|
|
745
|
+
{hasChanges === false ? t('common.actions.close') : t('common.actions.cancel')}
|
|
746
|
+
</Button>
|
|
747
|
+
<Button
|
|
748
|
+
className={cx('byline-form-actions-button', styles['actions-button'])}
|
|
749
|
+
size="sm"
|
|
750
|
+
type="submit"
|
|
751
|
+
disabled={
|
|
752
|
+
mutationsBlocked ||
|
|
753
|
+
discarding ||
|
|
754
|
+
hasChanges === false ||
|
|
755
|
+
isUploading ||
|
|
756
|
+
isSubmitting
|
|
704
757
|
}
|
|
758
|
+
aria-label={isSubmitting ? t('common.actions.save') : undefined}
|
|
705
759
|
>
|
|
706
|
-
{
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
760
|
+
{isUploading ? (
|
|
761
|
+
t('forms.actions.uploading')
|
|
762
|
+
) : (
|
|
763
|
+
<span className={cx('byline-form-save-content', styles['save-content'])}>
|
|
764
|
+
<span
|
|
765
|
+
className={cx(
|
|
766
|
+
'byline-form-save-label',
|
|
767
|
+
styles['save-label'],
|
|
768
|
+
isSubmitting && styles['save-label-hidden']
|
|
769
|
+
)}
|
|
770
|
+
>
|
|
771
|
+
{t('common.actions.save')}
|
|
772
|
+
</span>
|
|
773
|
+
{isSubmitting ? (
|
|
774
|
+
<span className={cx('byline-form-save-loader', styles['save-loader'])}>
|
|
775
|
+
<LoaderEllipsis size={28} aria-hidden="true" />
|
|
776
|
+
</span>
|
|
777
|
+
) : null}
|
|
778
|
+
</span>
|
|
779
|
+
)}
|
|
780
|
+
</Button>
|
|
781
|
+
{primaryStatus && onStatusChange && (
|
|
782
|
+
<div
|
|
783
|
+
className={cx('byline-form-actions-status-wrap', styles['actions-status-wrap'])}
|
|
784
|
+
>
|
|
785
|
+
<ComboButton
|
|
786
|
+
buttonClassName={cx(
|
|
787
|
+
'byline-form-actions-combo-button',
|
|
788
|
+
styles['actions-combo-button']
|
|
789
|
+
)}
|
|
790
|
+
triggerClassName={cx(
|
|
791
|
+
'byline-form-actions-combo-trigger',
|
|
792
|
+
styles['actions-combo-trigger']
|
|
793
|
+
)}
|
|
794
|
+
options={secondaryStatuses.map((s) => ({
|
|
795
|
+
label: isTerminal
|
|
796
|
+
? t('forms.actions.revertTo', { label: s.label ?? s.name })
|
|
797
|
+
: (s.verb ?? s.label ?? s.name),
|
|
798
|
+
value: s.name,
|
|
799
|
+
}))}
|
|
800
|
+
sideOffset={5}
|
|
801
|
+
size="sm"
|
|
802
|
+
type="button"
|
|
803
|
+
intent={isTerminal ? 'info' : 'success'}
|
|
804
|
+
disabled={mutationsBlocked || discarding || statusBusy}
|
|
805
|
+
onOptionSelect={async (value: string) => {
|
|
806
|
+
if (mutationBlockedRef.current) return
|
|
807
|
+
if (hasChanges) {
|
|
808
|
+
setShowUnsavedModal(true)
|
|
809
|
+
return
|
|
810
|
+
}
|
|
811
|
+
setStatusBusy(true)
|
|
812
|
+
try {
|
|
813
|
+
await onStatusChange(value)
|
|
814
|
+
} catch (error) {
|
|
815
|
+
onMutationError?.(error)
|
|
816
|
+
} finally {
|
|
817
|
+
setStatusBusy(false)
|
|
818
|
+
}
|
|
819
|
+
}}
|
|
820
|
+
onButtonClick={
|
|
821
|
+
isTerminal
|
|
822
|
+
? undefined
|
|
823
|
+
: async () => {
|
|
824
|
+
if (mutationBlockedRef.current) return
|
|
825
|
+
if (hasChanges) {
|
|
826
|
+
setShowUnsavedModal(true)
|
|
827
|
+
return
|
|
828
|
+
}
|
|
829
|
+
setStatusBusy(true)
|
|
830
|
+
try {
|
|
831
|
+
await onStatusChange(primaryStatus.name)
|
|
832
|
+
} catch (error) {
|
|
833
|
+
onMutationError?.(error)
|
|
834
|
+
} finally {
|
|
835
|
+
setStatusBusy(false)
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
>
|
|
840
|
+
{statusBusy
|
|
841
|
+
? '...'
|
|
842
|
+
: isTerminal
|
|
843
|
+
? (primaryStatus.label ?? primaryStatus.name)
|
|
844
|
+
: (primaryStatus.verb ?? primaryStatus.label ?? primaryStatus.name)}
|
|
845
|
+
</ComboButton>
|
|
846
|
+
</div>
|
|
847
|
+
)}
|
|
848
|
+
<DocumentActions
|
|
849
|
+
disabled={mutationsBlocked || discarding}
|
|
850
|
+
publishedVersion={publishedVersion}
|
|
851
|
+
onUnpublish={onUnpublish}
|
|
852
|
+
onDelete={onDelete}
|
|
853
|
+
onDuplicate={onDuplicate}
|
|
854
|
+
sourceTitle={
|
|
855
|
+
useAsTitle != null && initialData != null
|
|
856
|
+
? ((initialData as Record<string, unknown>)[useAsTitle] as
|
|
857
|
+
| string
|
|
858
|
+
| null
|
|
859
|
+
| undefined)
|
|
860
|
+
: null
|
|
861
|
+
}
|
|
862
|
+
onCopyToLocale={onCopyToLocale}
|
|
863
|
+
sourceLocale={contentLocale}
|
|
864
|
+
contentLocales={contentLocales}
|
|
865
|
+
hasUnsavedChanges={hasChanges}
|
|
866
|
+
onUnsavedChanges={() => setShowUnsavedModal(true)}
|
|
867
|
+
onDeleteLocale={onDeleteLocale}
|
|
868
|
+
defaultLocale={defaultLocale}
|
|
869
|
+
availableLocales={initialData?._availableVersionLocales as string[] | undefined}
|
|
870
|
+
scheduledPublicationState={scheduling.state}
|
|
871
|
+
onSchedulePublication={scheduling.openSchedule}
|
|
872
|
+
onConfirmScheduledPublication={scheduling.confirm}
|
|
873
|
+
onCancelScheduledPublication={scheduling.cancel}
|
|
874
|
+
/>
|
|
875
|
+
</div>
|
|
876
|
+
</div>
|
|
877
|
+
{(mutationIssue || scheduledPublicationsNeedReconfirmation) && (
|
|
878
|
+
<div
|
|
879
|
+
ref={warningRef}
|
|
880
|
+
tabIndex={-1}
|
|
881
|
+
role="alert"
|
|
882
|
+
aria-live="assertive"
|
|
883
|
+
className={cx('byline-document-concurrency', styles.concurrency)}
|
|
884
|
+
>
|
|
885
|
+
{mutationIssue && (
|
|
886
|
+
<Alert
|
|
887
|
+
intent="warning"
|
|
888
|
+
icon
|
|
889
|
+
close={false}
|
|
890
|
+
title={t(`documentConcurrency.${mutationIssue}Title`)}
|
|
891
|
+
>
|
|
892
|
+
<p>{t(`documentConcurrency.${mutationIssue}`)}</p>
|
|
893
|
+
{mutationIssue !== 'committed' && (
|
|
894
|
+
<Button
|
|
895
|
+
type="button"
|
|
896
|
+
disabled={discarding}
|
|
897
|
+
onClick={() => {
|
|
898
|
+
setReloadFailed(false)
|
|
899
|
+
setDiscarding(true)
|
|
900
|
+
}}
|
|
901
|
+
>
|
|
902
|
+
{t('documentConcurrency.reloadAction')}
|
|
903
|
+
</Button>
|
|
904
|
+
)}
|
|
905
|
+
{reloadFailed && <p>{t('documentConcurrency.reloadFailed')}</p>}
|
|
906
|
+
</Alert>
|
|
907
|
+
)}
|
|
908
|
+
{scheduledPublicationsNeedReconfirmation && (
|
|
909
|
+
<Alert
|
|
910
|
+
intent="warning"
|
|
911
|
+
icon
|
|
912
|
+
close={false}
|
|
913
|
+
title={t('documentConcurrency.schedulesTitle')}
|
|
914
|
+
>
|
|
915
|
+
<p>{t('documentConcurrency.schedules')}</p>
|
|
916
|
+
{scheduledPublicationsHref && (
|
|
917
|
+
<a href={scheduledPublicationsHref}>
|
|
918
|
+
{t('documentConcurrency.reviewSchedules')}
|
|
919
|
+
</a>
|
|
920
|
+
)}
|
|
921
|
+
</Alert>
|
|
922
|
+
)}
|
|
712
923
|
</div>
|
|
713
924
|
)}
|
|
714
|
-
<
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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}
|
|
925
|
+
<ScheduledPublicationNotice
|
|
926
|
+
state={scheduling.state}
|
|
927
|
+
timeZone={scheduling.timeZone}
|
|
928
|
+
busy={mutationsBlocked || discarding || scheduling.busy}
|
|
929
|
+
onConfirm={scheduling.confirm}
|
|
930
|
+
onReschedule={scheduling.openSchedule}
|
|
931
|
+
onCancel={scheduling.cancel}
|
|
739
932
|
/>
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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 ?? ''}
|
|
778
|
-
defaultLocale={defaultLocale}
|
|
779
|
-
activeLocale={contentLocale}
|
|
780
|
-
mode={mode}
|
|
781
|
-
slugifier={pathSlugifier}
|
|
782
|
-
sourceLocked={pathSourceLocked}
|
|
783
|
-
/>
|
|
784
|
-
)}
|
|
785
|
-
{tree && mode === 'edit' && typeof initialData?.id === 'string' && (
|
|
786
|
-
<TreePlacementWidget
|
|
787
|
-
collectionPath={collectionPath ?? ''}
|
|
788
|
-
documentId={initialData.id as string}
|
|
789
|
-
useAsTitle={useAsTitle}
|
|
790
|
-
/>
|
|
933
|
+
{scheduling.modal}
|
|
934
|
+
{restoreWarnings && restoreWarnings.length > 0 && (
|
|
935
|
+
<Alert
|
|
936
|
+
className="m-0 mt-4"
|
|
937
|
+
intent="warning"
|
|
938
|
+
icon={true}
|
|
939
|
+
close={false}
|
|
940
|
+
title={t('forms.restoreWarnings.title')}
|
|
941
|
+
>
|
|
942
|
+
<p>{t('forms.restoreWarnings.body', { count: restoreWarnings.length })}</p>
|
|
943
|
+
<ul>
|
|
944
|
+
{restoreWarnings.map((w) => (
|
|
945
|
+
<li key={w}>{w}</li>
|
|
946
|
+
))}
|
|
947
|
+
</ul>
|
|
948
|
+
</Alert>
|
|
791
949
|
)}
|
|
792
|
-
{
|
|
793
|
-
<
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
950
|
+
<div className={cx('byline-form-layout', styles.layout)}>
|
|
951
|
+
<div className={cx('byline-form-content', styles.content)}>
|
|
952
|
+
{layout.main.map((name) => renderItem(name))}
|
|
953
|
+
</div>
|
|
954
|
+
<div className={cx('byline-form-sidebar', styles.sidebar)}>
|
|
955
|
+
{showPath &&
|
|
956
|
+
(useAsPath ||
|
|
957
|
+
(typeof initialData?.path === 'string' && initialData.path.length > 0)) && (
|
|
958
|
+
<PathWidget
|
|
959
|
+
disabled={mutationsBlocked || discarding}
|
|
960
|
+
useAsPath={useAsPath}
|
|
961
|
+
collectionPath={collectionPath ?? ''}
|
|
962
|
+
defaultLocale={defaultLocale}
|
|
963
|
+
activeLocale={contentLocale}
|
|
964
|
+
mode={mode}
|
|
965
|
+
slugifier={pathSlugifier}
|
|
966
|
+
sourceLocked={pathSourceLocked}
|
|
967
|
+
/>
|
|
968
|
+
)}
|
|
969
|
+
{tree && mode === 'edit' && typeof initialData?.id === 'string' && (
|
|
970
|
+
<TreePlacementWidget
|
|
971
|
+
disabled={mutationsBlocked || discarding}
|
|
972
|
+
onMutationError={onMutationError}
|
|
973
|
+
onCommitted={onTreeMutationCommitted}
|
|
974
|
+
expectedRevision={observedRevision ?? initialData.revision}
|
|
975
|
+
collectionPath={collectionPath ?? ''}
|
|
976
|
+
documentId={initialData.id as string}
|
|
977
|
+
useAsTitle={useAsTitle}
|
|
978
|
+
/>
|
|
979
|
+
)}
|
|
980
|
+
{advertiseLocales && (
|
|
981
|
+
<AvailableLocalesWidget
|
|
982
|
+
disabled={mutationsBlocked || discarding}
|
|
983
|
+
contentLocales={contentLocales ?? []}
|
|
984
|
+
availableVersionLocales={
|
|
985
|
+
(initialData?._availableVersionLocales as string[] | undefined) ?? []
|
|
986
|
+
}
|
|
987
|
+
/>
|
|
988
|
+
)}
|
|
989
|
+
{(layout.sidebar ?? []).map((name) => renderItem(name))}
|
|
990
|
+
</div>
|
|
991
|
+
</div>
|
|
992
|
+
{showUnsavedModal && <UnsavedChangesModal onClose={() => setShowUnsavedModal(false)} />}
|
|
993
|
+
{!mutationsBlocked && !discarding && pendingSystemFieldsSubmit != null && (
|
|
994
|
+
<SystemFieldsConfirmModal
|
|
995
|
+
contentDirty={pendingSystemFieldsSubmit.contentDirty}
|
|
996
|
+
pathDirty={pendingSystemFieldsSubmit.pathDirty}
|
|
997
|
+
availableLocalesDirty={pendingSystemFieldsSubmit.availableLocalesDirty}
|
|
998
|
+
onCancel={() => setPendingSystemFieldsSubmit(null)}
|
|
999
|
+
onConfirm={() => {
|
|
1000
|
+
const payload = pendingSystemFieldsSubmit
|
|
1001
|
+
setPendingSystemFieldsSubmit(null)
|
|
1002
|
+
void submitPayload(payload)
|
|
1003
|
+
}}
|
|
798
1004
|
/>
|
|
799
1005
|
)}
|
|
800
|
-
{
|
|
801
|
-
|
|
1006
|
+
{guard.isBlocked && (
|
|
1007
|
+
<NavigationGuardModal onStay={guard.stay} onProceed={guard.proceed} />
|
|
1008
|
+
)}
|
|
1009
|
+
</form>
|
|
802
1010
|
</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>
|
|
1011
|
+
</>
|
|
819
1012
|
)
|
|
820
1013
|
}
|
|
821
1014
|
|