@byline/admin 4.1.0 → 4.3.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/array/array-field.d.ts +12 -2
- package/dist/fields/array/array-field.js +46 -20
- package/dist/fields/array/array-field.module.js +5 -1
- package/dist/fields/array/array-field_module.css +23 -0
- package/dist/fields/blocks/blocks-field.js +34 -12
- package/dist/fields/blocks/blocks-field.module.js +2 -0
- package/dist/fields/blocks/blocks-field_module.css +37 -0
- package/dist/fields/code/code-editor.d.ts +20 -0
- package/dist/fields/code/code-editor.js +239 -0
- package/dist/fields/code/code-field.d.ts +21 -0
- package/dist/fields/code/code-field.js +124 -0
- package/dist/fields/code/code-field.module.js +7 -0
- package/dist/fields/code/code-field_module.css +58 -0
- package/dist/fields/draggable-context-menu.js +2 -1
- package/dist/fields/field-admin.d.ts +15 -0
- package/dist/fields/field-admin.js +11 -0
- package/dist/fields/field-admin.test.node.d.ts +8 -0
- package/dist/fields/field-helpers.js +1 -0
- package/dist/fields/field-renderer.d.ts +11 -2
- package/dist/fields/field-renderer.js +21 -4
- package/dist/fields/group/group-field.d.ts +23 -2
- package/dist/fields/group/group-field.js +7 -3
- package/dist/fields/relation/relation-picker.js +8 -2
- package/dist/fields/select/select-field.js +4 -2
- package/dist/fields/select/select-field.module.js +1 -0
- package/dist/fields/select/select-field_module.css +4 -0
- package/dist/fields/sortable-item.d.ts +6 -0
- package/dist/fields/sortable-item.js +44 -25
- package/dist/fields/text/text-field_module.css +1 -0
- package/dist/fields/text-area/text-area-field_module.css +1 -0
- package/dist/forms/form-context.js +1 -1
- package/dist/forms/form-renderer.d.ts +1 -1
- package/dist/forms/form-renderer.js +3 -1
- package/dist/forms/tree-placement-widget.d.ts +1 -1
- package/package.json +23 -10
- package/src/fields/array/array-field.module.css +30 -0
- package/src/fields/array/array-field.tsx +74 -20
- package/src/fields/blocks/blocks-field.module.css +53 -0
- package/src/fields/blocks/blocks-field.tsx +38 -1
- package/src/fields/code/code-editor.tsx +246 -0
- package/src/fields/code/code-field.module.css +84 -0
- package/src/fields/code/code-field.tsx +191 -0
- package/src/fields/draggable-context-menu.tsx +9 -1
- package/src/fields/field-admin.test.node.ts +49 -0
- package/src/fields/field-admin.ts +39 -0
- package/src/fields/field-helpers.ts +1 -0
- package/src/fields/field-renderer.tsx +33 -2
- package/src/fields/field-services-types.ts +1 -1
- package/src/fields/group/group-field.tsx +29 -2
- package/src/fields/relation/relation-picker.tsx +11 -0
- package/src/fields/select/select-field.module.css +5 -0
- package/src/fields/select/select-field.tsx +9 -2
- package/src/fields/sortable-item.tsx +115 -34
- package/src/fields/text/text-field.module.css +1 -0
- package/src/fields/text-area/text-area-field.module.css +1 -0
- package/src/forms/form-context.tsx +9 -1
- package/src/forms/form-renderer.tsx +3 -1
- package/src/forms/tree-placement-widget.tsx +1 -1
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
ArrayField as ArrayFieldType,
|
|
11
11
|
BlocksField as BlocksFieldType,
|
|
12
12
|
Field,
|
|
13
|
+
FieldAdminConfig,
|
|
13
14
|
FieldComponentSlots,
|
|
14
15
|
GroupField as GroupFieldType,
|
|
15
16
|
RichTextEditorComponent,
|
|
@@ -21,6 +22,7 @@ import { useFormContext } from '../forms/form-context'
|
|
|
21
22
|
import { ArrayField } from './array/array-field'
|
|
22
23
|
import { BlocksField } from './blocks/blocks-field'
|
|
23
24
|
import { CheckboxField } from './checkbox/checkbox-field'
|
|
25
|
+
import { CodeField } from './code/code-field'
|
|
24
26
|
import { DateTimeField } from './datetime/datetime-field'
|
|
25
27
|
import styles from './field-renderer.module.css'
|
|
26
28
|
import { FileField } from './file/file-field'
|
|
@@ -67,6 +69,15 @@ interface FieldRendererProps {
|
|
|
67
69
|
* Ignored when `field.type !== 'richText'`.
|
|
68
70
|
*/
|
|
69
71
|
editor?: RichTextEditorComponent
|
|
72
|
+
/**
|
|
73
|
+
* Admin overrides for this field's *descendants*, keyed by dotted,
|
|
74
|
+
* index-free schema paths relative to this field ('answer',
|
|
75
|
+
* 'filesGroup.publicationFile'). Only meaningful when `field` is a
|
|
76
|
+
* structural `group` / `array` — the widget slices the map per child
|
|
77
|
+
* (see `sliceFieldAdmin`). `components` / `editor` above stay the
|
|
78
|
+
* overrides for this field itself.
|
|
79
|
+
*/
|
|
80
|
+
fieldAdmin?: Record<string, FieldAdminConfig>
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
export const FieldRenderer = ({
|
|
@@ -79,6 +90,7 @@ export const FieldRenderer = ({
|
|
|
79
90
|
contentLocale,
|
|
80
91
|
components,
|
|
81
92
|
editor,
|
|
93
|
+
fieldAdmin,
|
|
82
94
|
}: FieldRendererProps) => {
|
|
83
95
|
const path = basePath ? `${basePath}.${field.name}` : field.name
|
|
84
96
|
const htmlId = path.replace(/[[\].]/g, '-')
|
|
@@ -140,6 +152,18 @@ export const FieldRenderer = ({
|
|
|
140
152
|
components={components}
|
|
141
153
|
/>
|
|
142
154
|
)
|
|
155
|
+
case 'code':
|
|
156
|
+
return (
|
|
157
|
+
<CodeField
|
|
158
|
+
field={hideLabel ? { ...field, label: undefined } : field}
|
|
159
|
+
defaultValue={defaultValue}
|
|
160
|
+
onChange={handleChange}
|
|
161
|
+
path={path}
|
|
162
|
+
id={htmlId}
|
|
163
|
+
locale={isLocalised ? contentLocale : undefined}
|
|
164
|
+
components={components}
|
|
165
|
+
/>
|
|
166
|
+
)
|
|
143
167
|
case 'checkbox':
|
|
144
168
|
return (
|
|
145
169
|
<CheckboxField
|
|
@@ -276,8 +300,10 @@ export const FieldRenderer = ({
|
|
|
276
300
|
}
|
|
277
301
|
defaultValue={defaultValue}
|
|
278
302
|
path={path}
|
|
303
|
+
disableSorting={disableSorting}
|
|
279
304
|
collectionPath={collectionPath}
|
|
280
305
|
contentLocale={contentLocale}
|
|
306
|
+
fieldAdmin={fieldAdmin}
|
|
281
307
|
/>
|
|
282
308
|
)
|
|
283
309
|
case 'blocks':
|
|
@@ -300,6 +326,7 @@ export const FieldRenderer = ({
|
|
|
300
326
|
disableSorting={disableSorting}
|
|
301
327
|
collectionPath={collectionPath}
|
|
302
328
|
contentLocale={contentLocale}
|
|
329
|
+
fieldAdmin={fieldAdmin}
|
|
303
330
|
/>
|
|
304
331
|
)
|
|
305
332
|
default:
|
|
@@ -307,9 +334,13 @@ export const FieldRenderer = ({
|
|
|
307
334
|
}
|
|
308
335
|
}
|
|
309
336
|
|
|
310
|
-
// text and
|
|
337
|
+
// text, textArea, and code render the badge inside their own Label row;
|
|
311
338
|
// the outer wrapper is only needed for other field types.
|
|
312
|
-
const selfBadge =
|
|
339
|
+
const selfBadge =
|
|
340
|
+
field.type === 'text' ||
|
|
341
|
+
field.type === 'textArea' ||
|
|
342
|
+
field.type === 'code' ||
|
|
343
|
+
field.type === 'richText'
|
|
313
344
|
|
|
314
345
|
if (badge && !selfBadge) {
|
|
315
346
|
return (
|
|
@@ -62,7 +62,7 @@ export type UploadFieldFn = (
|
|
|
62
62
|
createDocument?: boolean
|
|
63
63
|
) => Promise<UploadedFileResult>
|
|
64
64
|
|
|
65
|
-
// --- Document tree (the `tree: true` primitive — docs/04-collections/
|
|
65
|
+
// --- Document tree (the `tree: true` primitive — docs/04-collections/04-document-trees.md) -----
|
|
66
66
|
|
|
67
67
|
/** One hydrated ancestor in a document's breadcrumb trail (root-first). */
|
|
68
68
|
export interface TreeAncestor {
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import { useMemo } from 'react'
|
|
10
10
|
|
|
11
|
-
import type { Field, GroupField as GroupFieldType } from '@byline/core'
|
|
11
|
+
import type { Field, FieldAdminConfig, GroupField as GroupFieldType } from '@byline/core'
|
|
12
12
|
import { ErrorText } from '@byline/ui/react'
|
|
13
13
|
import cx from 'classnames'
|
|
14
14
|
|
|
15
|
+
import { sliceFieldAdmin } from '../../fields/field-admin'
|
|
15
16
|
import { placeholderForField } from '../../fields/field-helpers'
|
|
16
17
|
import { FieldRenderer } from '../../fields/field-renderer'
|
|
17
18
|
import { useFieldError } from '../../forms/form-context'
|
|
@@ -32,6 +33,16 @@ interface GroupFieldProps {
|
|
|
32
33
|
field: GroupFieldType
|
|
33
34
|
defaultValue: any
|
|
34
35
|
path: string
|
|
36
|
+
/**
|
|
37
|
+
* Threaded to child fields — governs only the *drag* affordance of any
|
|
38
|
+
* `array` children (structural add/remove always renders; see ArrayField).
|
|
39
|
+
* Defaults to `true` (conservative): arrays inside plain schema groups
|
|
40
|
+
* stay drag-free. `BlocksField` passes `false` on its synthesized group so
|
|
41
|
+
* arrays directly inside blocks are fully sortable — safe because each
|
|
42
|
+
* `DraggableSortable` is an independent DndContext with grip-scoped
|
|
43
|
+
* listeners.
|
|
44
|
+
*/
|
|
45
|
+
disableSorting?: boolean
|
|
35
46
|
/**
|
|
36
47
|
* Collection path forwarded to upload-capable child fields (`file` / `image`),
|
|
37
48
|
* which need it to reach the `/upload` endpoint. Without it those fields fall
|
|
@@ -44,14 +55,27 @@ interface GroupFieldProps {
|
|
|
44
55
|
* locale badge.
|
|
45
56
|
*/
|
|
46
57
|
contentLocale?: string
|
|
58
|
+
/**
|
|
59
|
+
* Per-child-field admin overrides (`components` slots, richtext `editor`),
|
|
60
|
+
* keyed by dotted, index-free schema paths relative to this group
|
|
61
|
+
* ('caption', 'faq.answer'). Threaded by `BlocksField` from the site-wide
|
|
62
|
+
* `ClientConfig.blockAdmin` registry (block children render through a
|
|
63
|
+
* synthesized group) and by `FieldRenderer` for plain schema groups, whose
|
|
64
|
+
* map arrives pre-sliced from the collection admin config. Exact-name
|
|
65
|
+
* entries apply to the child itself; deeper entries are re-sliced and
|
|
66
|
+
* threaded on (see `sliceFieldAdmin`).
|
|
67
|
+
*/
|
|
68
|
+
fieldAdmin?: Record<string, FieldAdminConfig>
|
|
47
69
|
}
|
|
48
70
|
|
|
49
71
|
export const GroupField = ({
|
|
50
72
|
field,
|
|
51
73
|
defaultValue,
|
|
52
74
|
path,
|
|
75
|
+
disableSorting = true,
|
|
53
76
|
collectionPath,
|
|
54
77
|
contentLocale,
|
|
78
|
+
fieldAdmin,
|
|
55
79
|
}: GroupFieldProps) => {
|
|
56
80
|
const fieldError = useFieldError(field.name)
|
|
57
81
|
// Default value for a group field is a plain object: { rating: 5, comment: '...' }
|
|
@@ -91,9 +115,12 @@ export const GroupField = ({
|
|
|
91
115
|
field={innerField}
|
|
92
116
|
defaultValue={groupData[innerField.name]}
|
|
93
117
|
basePath={path}
|
|
94
|
-
disableSorting={
|
|
118
|
+
disableSorting={disableSorting}
|
|
95
119
|
collectionPath={collectionPath}
|
|
96
120
|
contentLocale={contentLocale}
|
|
121
|
+
components={fieldAdmin?.[innerField.name]?.components}
|
|
122
|
+
editor={fieldAdmin?.[innerField.name]?.editor}
|
|
123
|
+
fieldAdmin={sliceFieldAdmin(fieldAdmin, innerField.name)}
|
|
97
124
|
/>
|
|
98
125
|
)
|
|
99
126
|
})}
|
|
@@ -165,6 +165,13 @@ export const RelationPicker = ({
|
|
|
165
165
|
|
|
166
166
|
setLoading(true)
|
|
167
167
|
setError(null)
|
|
168
|
+
// Item-view sort: the target collection's `itemViewSort` (boot-validated)
|
|
169
|
+
// orders the picker independently of its list view's `defaultSort`.
|
|
170
|
+
// Passed as explicit params because the list server fn gives an explicit
|
|
171
|
+
// `order` top precedence; when absent the server falls back through
|
|
172
|
+
// `defaultSort` → `created_at desc` (or `order_key asc` for orderable
|
|
173
|
+
// collections) exactly as before.
|
|
174
|
+
const itemViewSort = targetAdminConfig?.itemViewSort
|
|
168
175
|
getCollectionDocuments({
|
|
169
176
|
collection: targetCollectionPath,
|
|
170
177
|
params: {
|
|
@@ -172,6 +179,9 @@ export const RelationPicker = ({
|
|
|
172
179
|
page_size: PAGE_SIZE,
|
|
173
180
|
query: query.length > 0 ? query : undefined,
|
|
174
181
|
fields: selectFields,
|
|
182
|
+
...(itemViewSort != null
|
|
183
|
+
? { order: String(itemViewSort.field), desc: itemViewSort.direction === 'desc' }
|
|
184
|
+
: {}),
|
|
175
185
|
},
|
|
176
186
|
})
|
|
177
187
|
.then((response: any) => {
|
|
@@ -202,6 +212,7 @@ export const RelationPicker = ({
|
|
|
202
212
|
pickerColumns,
|
|
203
213
|
getCollectionDocuments,
|
|
204
214
|
t,
|
|
215
|
+
targetAdminConfig?.itemViewSort,
|
|
205
216
|
])
|
|
206
217
|
|
|
207
218
|
const resolvedDisplayField =
|
|
@@ -38,10 +38,17 @@ export const SelectField = ({
|
|
|
38
38
|
return (
|
|
39
39
|
<div className={`byline-field-select ${field.name}`}>
|
|
40
40
|
{field.label && (
|
|
41
|
-
<Label
|
|
41
|
+
<Label
|
|
42
|
+
id={htmlId}
|
|
43
|
+
htmlFor={htmlId}
|
|
44
|
+
label={field.label}
|
|
45
|
+
required={!field.optional}
|
|
46
|
+
className={cx('byline-field-select-label', styles.label)}
|
|
47
|
+
/>
|
|
42
48
|
)}
|
|
43
49
|
<Select<string>
|
|
44
|
-
size="
|
|
50
|
+
size="xs"
|
|
51
|
+
variant="outlined"
|
|
45
52
|
id={htmlId}
|
|
46
53
|
name={field.name}
|
|
47
54
|
placeholder="Select an option"
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { type ReactNode, useState } from 'react'
|
|
9
|
+
import { type CSSProperties, type ReactNode, type Ref, useState } from 'react'
|
|
10
10
|
|
|
11
11
|
import { useTranslation } from '@byline/i18n/react'
|
|
12
12
|
import { ChevronDownIcon, GripperVerticalIcon, useSortable } from '@byline/ui/react'
|
|
@@ -15,44 +15,60 @@ import cx from 'classnames'
|
|
|
15
15
|
import { DraggableContextMenu } from './draggable-context-menu'
|
|
16
16
|
import styles from './sortable-item.module.css'
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Item chrome for repeating-structure entries (array items, block instances):
|
|
20
|
+
// a header carrying the label, the add-below/remove context menu, and a
|
|
21
|
+
// collapse toggle, above the item's rendered fields.
|
|
22
|
+
//
|
|
23
|
+
// Two variants share the frame:
|
|
24
|
+
// - `SortableItem` — adds the dnd-kit grip (drag handle). Must render
|
|
25
|
+
// inside a `DraggableSortable` (it calls `useSortable`).
|
|
26
|
+
// - `StaticItem` — no grip, no dnd hook. For contexts where drag is
|
|
27
|
+
// disabled but structural editing (add/remove/collapse) must remain,
|
|
28
|
+
// e.g. arrays nested inside another array's items.
|
|
29
|
+
//
|
|
30
|
+
// They are separate components (not a boolean prop on one component)
|
|
31
|
+
// because `useSortable` is a hook — it cannot be called conditionally, and
|
|
32
|
+
// it throws outside a DndContext.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
interface ItemFrameProps {
|
|
26
36
|
label: ReactNode
|
|
27
37
|
children: ReactNode
|
|
28
38
|
onAddBelow?: () => void
|
|
29
39
|
onRemove?: () => void
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
})
|
|
40
|
+
/** Drag handle slot — rendered leading the header when provided. */
|
|
41
|
+
grip?: ReactNode
|
|
42
|
+
rootRef?: Ref<HTMLDivElement>
|
|
43
|
+
style?: CSSProperties
|
|
44
|
+
dragging?: boolean
|
|
45
|
+
/** Extra root class, e.g. the static variant marker. */
|
|
46
|
+
rootClassName?: string
|
|
47
|
+
}
|
|
39
48
|
|
|
49
|
+
const ItemFrame = ({
|
|
50
|
+
label,
|
|
51
|
+
children,
|
|
52
|
+
onAddBelow,
|
|
53
|
+
onRemove,
|
|
54
|
+
grip,
|
|
55
|
+
rootRef,
|
|
56
|
+
style,
|
|
57
|
+
dragging = false,
|
|
58
|
+
rootClassName,
|
|
59
|
+
}: ItemFrameProps) => {
|
|
60
|
+
const { t } = useTranslation('byline-admin')
|
|
40
61
|
const [collapsed, setCollapsed] = useState(false)
|
|
41
62
|
|
|
42
|
-
const style = {
|
|
43
|
-
transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
|
|
44
|
-
transition,
|
|
45
|
-
zIndex: isDragging ? 10 : 'auto',
|
|
46
|
-
}
|
|
47
|
-
|
|
48
63
|
return (
|
|
49
64
|
<div
|
|
50
|
-
ref={
|
|
65
|
+
ref={rootRef}
|
|
51
66
|
style={style}
|
|
52
67
|
className={cx(
|
|
53
68
|
'byline-sortable',
|
|
54
69
|
styles.root,
|
|
55
|
-
|
|
70
|
+
rootClassName,
|
|
71
|
+
dragging && ['byline-sortable-dragging', styles.dragging],
|
|
56
72
|
collapsed && ['byline-sortable-collapsed', styles.collapsed]
|
|
57
73
|
)}
|
|
58
74
|
>
|
|
@@ -63,14 +79,7 @@ export const SortableItem = ({
|
|
|
63
79
|
!collapsed && ['byline-sortable-header-expanded', styles['header-expanded']]
|
|
64
80
|
)}
|
|
65
81
|
>
|
|
66
|
-
|
|
67
|
-
type="button"
|
|
68
|
-
className={cx('byline-sortable-grip', styles.grip)}
|
|
69
|
-
{...attributes}
|
|
70
|
-
{...listeners}
|
|
71
|
-
>
|
|
72
|
-
<GripperVerticalIcon className={cx('byline-sortable-grip-icon', styles['grip-icon'])} />
|
|
73
|
-
</button>
|
|
82
|
+
{grip}
|
|
74
83
|
<div className={cx('byline-sortable-label', styles.label)}>{label}</div>
|
|
75
84
|
<DraggableContextMenu onAddBelow={onAddBelow} onRemove={onRemove} />
|
|
76
85
|
<button
|
|
@@ -104,3 +113,75 @@ export const SortableItem = ({
|
|
|
104
113
|
</div>
|
|
105
114
|
)
|
|
106
115
|
}
|
|
116
|
+
|
|
117
|
+
export const SortableItem = ({
|
|
118
|
+
id,
|
|
119
|
+
label,
|
|
120
|
+
children,
|
|
121
|
+
onAddBelow,
|
|
122
|
+
onRemove,
|
|
123
|
+
}: {
|
|
124
|
+
id: string
|
|
125
|
+
label: ReactNode
|
|
126
|
+
children: ReactNode
|
|
127
|
+
onAddBelow?: () => void
|
|
128
|
+
onRemove?: () => void
|
|
129
|
+
}) => {
|
|
130
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
131
|
+
id,
|
|
132
|
+
transition: {
|
|
133
|
+
duration: 250,
|
|
134
|
+
easing: 'cubic-bezier(0, 0.2, 0.2, 1)',
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const style = {
|
|
139
|
+
transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
|
|
140
|
+
transition,
|
|
141
|
+
zIndex: isDragging ? 10 : 'auto',
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<ItemFrame
|
|
146
|
+
label={label}
|
|
147
|
+
onAddBelow={onAddBelow}
|
|
148
|
+
onRemove={onRemove}
|
|
149
|
+
rootRef={setNodeRef}
|
|
150
|
+
style={style}
|
|
151
|
+
dragging={isDragging}
|
|
152
|
+
grip={
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
className={cx('byline-sortable-grip', styles.grip)}
|
|
156
|
+
{...attributes}
|
|
157
|
+
{...listeners}
|
|
158
|
+
>
|
|
159
|
+
<GripperVerticalIcon className={cx('byline-sortable-grip-icon', styles['grip-icon'])} />
|
|
160
|
+
</button>
|
|
161
|
+
}
|
|
162
|
+
>
|
|
163
|
+
{children}
|
|
164
|
+
</ItemFrame>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export const StaticItem = ({
|
|
169
|
+
label,
|
|
170
|
+
children,
|
|
171
|
+
onAddBelow,
|
|
172
|
+
onRemove,
|
|
173
|
+
}: {
|
|
174
|
+
label: ReactNode
|
|
175
|
+
children: ReactNode
|
|
176
|
+
onAddBelow?: () => void
|
|
177
|
+
onRemove?: () => void
|
|
178
|
+
}) => (
|
|
179
|
+
<ItemFrame
|
|
180
|
+
label={label}
|
|
181
|
+
onAddBelow={onAddBelow}
|
|
182
|
+
onRemove={onRemove}
|
|
183
|
+
rootClassName="byline-sortable-static"
|
|
184
|
+
>
|
|
185
|
+
{children}
|
|
186
|
+
</ItemFrame>
|
|
187
|
+
)
|
|
@@ -316,7 +316,15 @@ export const FormProvider = ({
|
|
|
316
316
|
const getPatches = useCallback(() => patchesRef.current, [])
|
|
317
317
|
const appendPatch = useCallback(
|
|
318
318
|
(patch: DocumentPatch) => {
|
|
319
|
-
|
|
319
|
+
// Snapshot the patch at append time. Structural patches (array.insert,
|
|
320
|
+
// block add) carry item objects that are ALSO placed into the form
|
|
321
|
+
// store — and `setNestedValue` mutates store nodes in place, so a
|
|
322
|
+
// later nested write inside the item (e.g. adding an array item to a
|
|
323
|
+
// block added this session) would silently rewrite the queued patch.
|
|
324
|
+
// Serialized at save time, the block insert would then already contain
|
|
325
|
+
// the array items AND the array.insert patches would re-add them —
|
|
326
|
+
// duplicating items server-side (caught by e2e/array-in-block.spec.ts).
|
|
327
|
+
patchesRef.current = [...patchesRef.current, structuredClone(patch)]
|
|
320
328
|
// Mark a generic dirty flag so hasChanges() becomes true even
|
|
321
329
|
// for patches that don't correspond to a specific field.set.
|
|
322
330
|
dirtyFields.current.add('__patch__')
|
|
@@ -24,6 +24,7 @@ import { useTranslation } from '@byline/i18n/react'
|
|
|
24
24
|
import { Alert, Button, ComboButton } from '@byline/ui/react'
|
|
25
25
|
import cx from 'classnames'
|
|
26
26
|
|
|
27
|
+
import { sliceFieldAdmin } from '../fields/field-admin'
|
|
27
28
|
import { FieldRenderer } from '../fields/field-renderer'
|
|
28
29
|
import { useBylineFieldServices } from '../fields/field-services-context'
|
|
29
30
|
import { AdminGroup } from '../presentation/group'
|
|
@@ -135,7 +136,7 @@ export interface FormRendererProps {
|
|
|
135
136
|
* Opts the document-tree placement widget into the sidebar (above the
|
|
136
137
|
* available-locales widget). Sourced from `CollectionDefinition.tree` by the
|
|
137
138
|
* caller. Renders only in edit mode (placement needs a persisted document)
|
|
138
|
-
* and only when the host wires the tree services. See docs/04-collections/
|
|
139
|
+
* and only when the host wires the tree services. See docs/04-collections/04-document-trees.md.
|
|
139
140
|
*/
|
|
140
141
|
tree?: boolean
|
|
141
142
|
headingLabel?: string
|
|
@@ -488,6 +489,7 @@ const FormContent = ({
|
|
|
488
489
|
contentLocale={contentLocale}
|
|
489
490
|
components={adminConfig?.fields?.[field.name]?.components}
|
|
490
491
|
editor={adminConfig?.fields?.[field.name]?.editor}
|
|
492
|
+
fieldAdmin={sliceFieldAdmin(adminConfig?.fields, field.name)}
|
|
491
493
|
/>
|
|
492
494
|
)
|
|
493
495
|
}
|
|
@@ -30,7 +30,7 @@ export interface TreePlacementWidgetProps {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* Sidebar widget for placing the current document within its collection's
|
|
33
|
-
* single-parent document tree (the `tree: true` primitive — docs/04-collections/
|
|
33
|
+
* single-parent document tree (the `tree: true` primitive — docs/04-collections/04-document-trees.md).
|
|
34
34
|
*
|
|
35
35
|
* The tree is document-grain and **unversioned**, so changes here write
|
|
36
36
|
* immediately (independent of the form's content save). The editor picks a
|