@byline/admin 4.16.0 → 4.17.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/relation/relation-field.js +3 -2
- package/dist/fields/relation/relation-many-field.js +3 -2
- package/dist/fields/relation/relation-picker.d.ts +2 -2
- package/dist/fields/relation/relation-summary.d.ts +2 -2
- package/dist/forms/document-actions.js +30 -25
- package/dist/forms/form-renderer.d.ts +17 -3
- package/dist/forms/form-renderer.js +18 -9
- package/dist/forms/tree-placement-widget.js +3 -2
- package/dist/forms/use-form-layout.d.ts +3 -3
- package/package.json +11 -13
- package/src/fields/relation/relation-field.tsx +5 -3
- package/src/fields/relation/relation-many-field.tsx +5 -3
- package/src/fields/relation/relation-picker.tsx +2 -2
- package/src/fields/relation/relation-summary.tsx +2 -2
- package/src/forms/document-actions.test.tsx +157 -0
- package/src/forms/document-actions.tsx +93 -74
- package/src/forms/form-renderer-capabilities.test.tsx +155 -0
- package/src/forms/form-renderer-submit.test.tsx +194 -0
- package/src/forms/form-renderer.tsx +59 -25
- package/src/forms/tree-placement-widget.tsx +3 -2
- package/src/forms/use-form-layout.ts +3 -3
- package/dist/forms/__probe-nested.test.node.d.ts +0 -1
- package/dist/forms/__probe-two.test.node.d.ts +0 -1
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
12
12
|
|
|
13
13
|
import type {
|
|
14
|
-
CollectionAdminConfig,
|
|
15
14
|
Field,
|
|
15
|
+
FormAdminConfig,
|
|
16
16
|
GroupDefinition,
|
|
17
17
|
RowDefinition,
|
|
18
18
|
TabSetDefinition,
|
|
@@ -82,7 +82,7 @@ export interface SystemFieldsSubmitPayload {
|
|
|
82
82
|
export interface FormRendererProps {
|
|
83
83
|
mode: 'create' | 'edit'
|
|
84
84
|
fields: Field[]
|
|
85
|
-
onSubmit: (data: any) => void
|
|
85
|
+
onSubmit: (data: any) => void | Promise<void>
|
|
86
86
|
onCancel: () => void
|
|
87
87
|
onStatusChange?: (nextStatus: string) => Promise<void>
|
|
88
88
|
onUnpublish?: () => Promise<void>
|
|
@@ -124,7 +124,7 @@ export interface FormRendererProps {
|
|
|
124
124
|
workflowStatuses?: WorkflowStatus[]
|
|
125
125
|
publishedVersion?: PublishedVersionInfo | null
|
|
126
126
|
initialData?: Record<string, any>
|
|
127
|
-
adminConfig?:
|
|
127
|
+
adminConfig?: FormAdminConfig
|
|
128
128
|
/**
|
|
129
129
|
* Name of the schema field to render as the live form heading.
|
|
130
130
|
* Sourced from `CollectionDefinition.useAsTitle` by the caller.
|
|
@@ -136,6 +136,20 @@ export interface FormRendererProps {
|
|
|
136
136
|
* present the path widget renders in the sidebar.
|
|
137
137
|
*/
|
|
138
138
|
useAsPath?: string
|
|
139
|
+
/**
|
|
140
|
+
* Whether the system path widget may render in the sidebar. Defaults to
|
|
141
|
+
* `true`, which preserves the historical behaviour of showing the widget
|
|
142
|
+
* whenever `useAsPath` is declared or the document envelope carries a
|
|
143
|
+
* `path`. Set `false` for a resource whose path is internal metadata and
|
|
144
|
+
* must never be presented or edited.
|
|
145
|
+
*/
|
|
146
|
+
showPath?: boolean
|
|
147
|
+
/**
|
|
148
|
+
* Explicit form heading, used verbatim. Overrides both `useAsTitle`'s live
|
|
149
|
+
* value and the create/edit wording derived from `mode` — for a resource
|
|
150
|
+
* whose identity does not change when it is first materialised.
|
|
151
|
+
*/
|
|
152
|
+
heading?: string
|
|
139
153
|
/**
|
|
140
154
|
* Opts the available-locales widget into the sidebar (below the path
|
|
141
155
|
* widget). Sourced from `CollectionDefinition.advertiseLocales` by the
|
|
@@ -203,6 +217,8 @@ const FormContent = ({
|
|
|
203
217
|
adminConfig,
|
|
204
218
|
useAsTitle,
|
|
205
219
|
useAsPath,
|
|
220
|
+
showPath = true,
|
|
221
|
+
heading,
|
|
206
222
|
advertiseLocales,
|
|
207
223
|
tree,
|
|
208
224
|
headingLabel,
|
|
@@ -245,6 +261,8 @@ const FormContent = ({
|
|
|
245
261
|
const [hasChanges, setHasChanges] = useState(hasChangesFn())
|
|
246
262
|
const [statusBusy, setStatusBusy] = useState(false)
|
|
247
263
|
const [isUploading, setIsUploading] = useState(false)
|
|
264
|
+
const submittingRef = useRef(false)
|
|
265
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
248
266
|
// Block-only "save first" guard. Set true when the editor triggers a
|
|
249
267
|
// guarded action (status change, duplicate, copy-to-locale) while the form
|
|
250
268
|
// is dirty — those actions operate on the saved version, so unsaved edits
|
|
@@ -336,7 +354,8 @@ const FormContent = ({
|
|
|
336
354
|
|
|
337
355
|
// Live document heading — tracks the useAsTitle field as the user types
|
|
338
356
|
const liveTitle = useFieldValue<string>(useAsTitle ?? '')
|
|
339
|
-
const
|
|
357
|
+
const computedHeading =
|
|
358
|
+
heading ||
|
|
340
359
|
liveTitle ||
|
|
341
360
|
(headingLabel
|
|
342
361
|
? mode === 'create'
|
|
@@ -379,13 +398,27 @@ const FormContent = ({
|
|
|
379
398
|
}
|
|
380
399
|
}
|
|
381
400
|
|
|
382
|
-
//
|
|
383
|
-
//
|
|
401
|
+
// Await the host handler. Resolution means the save succeeded and the clean
|
|
402
|
+
// baseline can be committed; rejection preserves dirty state so the editor
|
|
403
|
+
// does not lose work and the navigation guard keeps blocking. Host handlers
|
|
404
|
+
// that surface their own toast MUST rethrow afterwards.
|
|
405
|
+
// Re-entry guard lives in a ref so the callback identity does not change
|
|
406
|
+
// mid-flight; the mirrored state drives the Save button's disabled prop.
|
|
384
407
|
const submitPayload = useCallback(
|
|
385
|
-
(payload: SystemFieldsSubmitPayload) => {
|
|
386
|
-
if (
|
|
387
|
-
|
|
408
|
+
async (payload: SystemFieldsSubmitPayload) => {
|
|
409
|
+
if (typeof onSubmit !== 'function') return
|
|
410
|
+
if (submittingRef.current) return
|
|
411
|
+
submittingRef.current = true
|
|
412
|
+
setIsSubmitting(true)
|
|
413
|
+
try {
|
|
414
|
+
await onSubmit(payload)
|
|
388
415
|
resetHasChanges()
|
|
416
|
+
} catch {
|
|
417
|
+
// Intentionally swallowed here — the host has already reported the
|
|
418
|
+
// failure to the user. Dirty state is preserved by not resetting.
|
|
419
|
+
} finally {
|
|
420
|
+
submittingRef.current = false
|
|
421
|
+
setIsSubmitting(false)
|
|
389
422
|
}
|
|
390
423
|
},
|
|
391
424
|
[onSubmit, resetHasChanges]
|
|
@@ -482,7 +515,7 @@ const FormContent = ({
|
|
|
482
515
|
return
|
|
483
516
|
}
|
|
484
517
|
|
|
485
|
-
submitPayload(payload)
|
|
518
|
+
await submitPayload(payload)
|
|
486
519
|
})()
|
|
487
520
|
}
|
|
488
521
|
|
|
@@ -582,7 +615,7 @@ const FormContent = ({
|
|
|
582
615
|
aria-busy={isUploading}
|
|
583
616
|
>
|
|
584
617
|
<div className={cx('byline-form-heading-row', styles['heading-row'])}>
|
|
585
|
-
<h1 className={cx('byline-form-heading', styles.heading)}>{
|
|
618
|
+
<h1 className={cx('byline-form-heading', styles.heading)}>{computedHeading}</h1>
|
|
586
619
|
{/* Source-locale anchor indicator removed pending heading-layout work.
|
|
587
620
|
To re-enable: render `<SourceLocaleBadge locale={sourceLocale} />`
|
|
588
621
|
here from `initialData.sourceLocale` (mismatch-only is the intended
|
|
@@ -615,7 +648,7 @@ const FormContent = ({
|
|
|
615
648
|
className={cx('byline-form-actions-button', styles['actions-button'])}
|
|
616
649
|
size="sm"
|
|
617
650
|
type="submit"
|
|
618
|
-
disabled={hasChanges === false || isUploading}
|
|
651
|
+
disabled={hasChanges === false || isUploading || isSubmitting}
|
|
619
652
|
>
|
|
620
653
|
{isUploading ? t('forms.actions.uploading') : t('common.actions.save')}
|
|
621
654
|
</Button>
|
|
@@ -736,18 +769,19 @@ const FormContent = ({
|
|
|
736
769
|
{layout.main.map((name) => renderItem(name))}
|
|
737
770
|
</div>
|
|
738
771
|
<div className={cx('byline-form-sidebar', styles.sidebar)}>
|
|
739
|
-
{
|
|
740
|
-
(
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
+
)}
|
|
751
785
|
{tree && mode === 'edit' && typeof initialData?.id === 'string' && (
|
|
752
786
|
<TreePlacementWidget
|
|
753
787
|
collectionPath={collectionPath ?? ''}
|
|
@@ -776,7 +810,7 @@ const FormContent = ({
|
|
|
776
810
|
onConfirm={() => {
|
|
777
811
|
const payload = pendingSystemFieldsSubmit
|
|
778
812
|
setPendingSystemFieldsSubmit(null)
|
|
779
|
-
submitPayload(payload)
|
|
813
|
+
void submitPayload(payload)
|
|
780
814
|
}}
|
|
781
815
|
/>
|
|
782
816
|
)}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { useCallback, useEffect, useState } from 'react'
|
|
12
12
|
|
|
13
|
-
import { getCollectionDefinition } from '@byline/core'
|
|
13
|
+
import { getCollectionDefinition, isSingleton } from '@byline/core'
|
|
14
14
|
import { useTranslation } from '@byline/i18n/react'
|
|
15
15
|
import { Button } from '@byline/ui/react'
|
|
16
16
|
import cx from 'clsx'
|
|
@@ -49,7 +49,8 @@ export const TreePlacementWidget = ({
|
|
|
49
49
|
}: TreePlacementWidgetProps) => {
|
|
50
50
|
const { t } = useTranslation('byline-admin')
|
|
51
51
|
const { getTreeAncestors, getTreeParent, placeTreeNode } = useBylineFieldServices()
|
|
52
|
-
const
|
|
52
|
+
const definition = getCollectionDefinition(collectionPath)
|
|
53
|
+
const targetDefinition = definition != null && !isSingleton(definition) ? definition : null
|
|
53
54
|
|
|
54
55
|
const [parent, setParent] = useState<{ id: string; title: string } | null>(null)
|
|
55
56
|
// Whether the document has an edge row at all. `false` = *unplaced* (no row);
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
import { useMemo } from 'react'
|
|
12
12
|
|
|
13
13
|
import type {
|
|
14
|
-
CollectionAdminConfig,
|
|
15
14
|
Field,
|
|
15
|
+
FormAdminConfig,
|
|
16
16
|
GroupDefinition,
|
|
17
17
|
LayoutDefinition,
|
|
18
18
|
RowDefinition,
|
|
@@ -50,7 +50,7 @@ export interface FormLayout {
|
|
|
50
50
|
* the `seen` set guards against a config that references a row/group cycle.
|
|
51
51
|
*/
|
|
52
52
|
export function buildFieldToTabPath(
|
|
53
|
-
adminConfig:
|
|
53
|
+
adminConfig: FormAdminConfig | undefined,
|
|
54
54
|
fieldByName: Map<string, Field>,
|
|
55
55
|
rowByName: Map<string, RowDefinition>,
|
|
56
56
|
groupByName: Map<string, GroupDefinition>
|
|
@@ -90,7 +90,7 @@ export function buildFieldToTabPath(
|
|
|
90
90
|
* Rebuilt only when `adminConfig` / `fields` change.
|
|
91
91
|
*/
|
|
92
92
|
export function useFormLayout(
|
|
93
|
-
adminConfig:
|
|
93
|
+
adminConfig: FormAdminConfig | undefined,
|
|
94
94
|
fields: Field[]
|
|
95
95
|
): FormLayout {
|
|
96
96
|
const fieldByName = useMemo(() => {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|