@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
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.3.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -149,32 +149,45 @@
|
|
|
149
149
|
"src/"
|
|
150
150
|
],
|
|
151
151
|
"dependencies": {
|
|
152
|
-
"@
|
|
152
|
+
"@codemirror/commands": "^6.10.4",
|
|
153
|
+
"@codemirror/lang-css": "^6.3.1",
|
|
154
|
+
"@codemirror/lang-html": "^6.4.11",
|
|
155
|
+
"@codemirror/lang-javascript": "^6.2.5",
|
|
156
|
+
"@codemirror/lang-json": "^6.0.2",
|
|
157
|
+
"@codemirror/lang-markdown": "^6.5.1",
|
|
158
|
+
"@codemirror/lang-python": "^6.2.1",
|
|
159
|
+
"@codemirror/lang-sql": "^6.10.0",
|
|
160
|
+
"@codemirror/lang-yaml": "^6.1.3",
|
|
161
|
+
"@codemirror/language": "^6.12.4",
|
|
162
|
+
"@codemirror/state": "^6.7.1",
|
|
163
|
+
"@codemirror/view": "^6.43.6",
|
|
164
|
+
"@lezer/highlight": "^1.2.3",
|
|
165
|
+
"@tanstack/react-form-start": "^1.33.2",
|
|
153
166
|
"classnames": "^2.5.1",
|
|
154
167
|
"jose": "^6.2.3",
|
|
155
|
-
"react-diff-viewer-continued": "^4.
|
|
168
|
+
"react-diff-viewer-continued": "^4.4.0",
|
|
156
169
|
"uuid": "^14.0.1",
|
|
157
170
|
"zod": "^4.4.3",
|
|
158
|
-
"@byline/core": "4.
|
|
159
|
-
"@byline/
|
|
160
|
-
"@byline/
|
|
161
|
-
"@byline/
|
|
171
|
+
"@byline/core": "4.3.0",
|
|
172
|
+
"@byline/auth": "4.3.0",
|
|
173
|
+
"@byline/i18n": "4.3.0",
|
|
174
|
+
"@byline/ui": "4.3.0"
|
|
162
175
|
},
|
|
163
176
|
"peerDependencies": {
|
|
164
177
|
"react": "^19.0.0",
|
|
165
178
|
"react-dom": "^19.0.0"
|
|
166
179
|
},
|
|
167
180
|
"devDependencies": {
|
|
168
|
-
"@biomejs/biome": "2.5.
|
|
181
|
+
"@biomejs/biome": "2.5.4",
|
|
169
182
|
"@rsbuild/plugin-react": "^2.1.0",
|
|
170
183
|
"@rslib/core": "^0.23.2",
|
|
171
|
-
"@types/node": "^26.1.
|
|
184
|
+
"@types/node": "^26.1.1",
|
|
172
185
|
"@types/react": "19.2.17",
|
|
173
186
|
"@types/react-dom": "19.2.3",
|
|
174
187
|
"react": "^19.2.7",
|
|
175
188
|
"react-dom": "^19.2.7",
|
|
176
189
|
"rimraf": "^6.1.3",
|
|
177
|
-
"tsx": "^4.23.
|
|
190
|
+
"tsx": "^4.23.1",
|
|
178
191
|
"typescript": "^7.0.2",
|
|
179
192
|
"typescript-plugin-css-modules": "^5.2.0",
|
|
180
193
|
"vitest": "^4.1.10"
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* .byline-field-array — outer wrapper
|
|
6
6
|
* .byline-field-array-title — array label heading
|
|
7
7
|
* .byline-field-array-stack — vertical layout for items + add row
|
|
8
|
+
* .byline-field-array-add-row — add-button row at the bottom
|
|
9
|
+
* .byline-field-array-add-label — clickable text label in the add row
|
|
8
10
|
* .byline-field-array-card — disable-sorting fallback card per item
|
|
9
11
|
* .byline-field-array-group-header — sub-group title above grouped fields
|
|
10
12
|
* .byline-field-array-group-fields — vertical stack of fields inside an item
|
|
@@ -46,3 +48,31 @@
|
|
|
46
48
|
flex-direction: column;
|
|
47
49
|
gap: var(--spacing-16);
|
|
48
50
|
}
|
|
51
|
+
|
|
52
|
+
.add-row,
|
|
53
|
+
:global(.byline-field-array-add-row) {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
font-size: 0.9rem;
|
|
57
|
+
color: var(--muted);
|
|
58
|
+
gap: var(--spacing-8);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Text-styled companion button for the add row's label — clickable like the
|
|
62
|
+
icon button. Hovering anywhere on the row brightens the label text. */
|
|
63
|
+
.add-label,
|
|
64
|
+
:global(.byline-field-array-add-label) {
|
|
65
|
+
appearance: none;
|
|
66
|
+
background: none;
|
|
67
|
+
border: none;
|
|
68
|
+
padding: 0;
|
|
69
|
+
font: inherit;
|
|
70
|
+
color: inherit;
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
transition: color 120ms ease;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.add-row:hover .add-label,
|
|
76
|
+
:global(.byline-field-array-add-row):hover :global(.byline-field-array-add-label) {
|
|
77
|
+
color: var(--text);
|
|
78
|
+
}
|
|
@@ -8,20 +8,22 @@
|
|
|
8
8
|
|
|
9
9
|
import { useEffect, useState } from 'react'
|
|
10
10
|
|
|
11
|
-
import type { ArrayField as ArrayFieldType, Field } from '@byline/core'
|
|
11
|
+
import type { ArrayField as ArrayFieldType, Field, FieldAdminConfig } from '@byline/core'
|
|
12
12
|
import { useTranslation } from '@byline/i18n/react'
|
|
13
13
|
import { DraggableSortable, IconButton, moveItem, PlusIcon } from '@byline/ui/react'
|
|
14
14
|
import cx from 'classnames'
|
|
15
15
|
|
|
16
|
+
import { sliceFieldAdmin } from '../../fields/field-admin'
|
|
16
17
|
import { defaultScalarForField } from '../../fields/field-helpers'
|
|
17
18
|
import { FieldRenderer } from '../../fields/field-renderer'
|
|
18
|
-
import { SortableItem } from '../../fields/sortable-item'
|
|
19
|
+
import { SortableItem, StaticItem } from '../../fields/sortable-item'
|
|
19
20
|
import { useFormContext } from '../../forms/form-context'
|
|
20
21
|
import styles from './array-field.module.css'
|
|
21
22
|
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
23
|
-
// ArrayField — renders `type: 'array'` fields.
|
|
24
|
-
//
|
|
24
|
+
// ArrayField — renders `type: 'array'` fields. Each item renders every child
|
|
25
|
+
// field declared in the array's `fields` (value fields and/or groups, in any
|
|
26
|
+
// combination). Supports D&D.
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
|
|
27
29
|
export const ArrayField = ({
|
|
@@ -31,6 +33,7 @@ export const ArrayField = ({
|
|
|
31
33
|
disableSorting = false,
|
|
32
34
|
collectionPath,
|
|
33
35
|
contentLocale,
|
|
36
|
+
fieldAdmin,
|
|
34
37
|
}: {
|
|
35
38
|
field: ArrayFieldType
|
|
36
39
|
defaultValue: any
|
|
@@ -49,6 +52,16 @@ export const ArrayField = ({
|
|
|
49
52
|
* can render their locale badge.
|
|
50
53
|
*/
|
|
51
54
|
contentLocale?: string
|
|
55
|
+
/**
|
|
56
|
+
* Admin overrides for the array's child fields, keyed by dotted,
|
|
57
|
+
* index-free schema paths relative to this array ('answer',
|
|
58
|
+
* 'filesGroup.publicationFile'). Schema paths address declarations, so
|
|
59
|
+
* one entry applies to that field in every item. Arrives pre-sliced from
|
|
60
|
+
* the enclosing widget (`FieldRenderer` / `GroupField`); exact-name
|
|
61
|
+
* entries apply to the child, deeper entries are re-sliced and threaded
|
|
62
|
+
* on (see `sliceFieldAdmin`).
|
|
63
|
+
*/
|
|
64
|
+
fieldAdmin?: Record<string, FieldAdminConfig>
|
|
52
65
|
}) => {
|
|
53
66
|
const { appendPatch, getFieldValue, getFieldValues, setFieldStore } = useFormContext()
|
|
54
67
|
const { t } = useTranslation('byline-admin')
|
|
@@ -207,8 +220,11 @@ export const ArrayField = ({
|
|
|
207
220
|
const initial = item[childField.name]
|
|
208
221
|
|
|
209
222
|
if (childField.type === 'group' && childField.fields && childField.fields.length > 0) {
|
|
210
|
-
// Group child — render its inner fields with the group's sub-object
|
|
223
|
+
// Group child — render its inner fields with the group's sub-object.
|
|
224
|
+
// fieldAdmin slices one level per structural hop: the group's
|
|
225
|
+
// descendant map first, then each inner field's own slice.
|
|
211
226
|
const groupData = initial && typeof initial === 'object' ? initial : {}
|
|
227
|
+
const groupAdmin = sliceFieldAdmin(fieldAdmin, childField.name)
|
|
212
228
|
return (
|
|
213
229
|
<div
|
|
214
230
|
key={childField.name}
|
|
@@ -228,6 +244,9 @@ export const ArrayField = ({
|
|
|
228
244
|
disableSorting={true}
|
|
229
245
|
collectionPath={collectionPath}
|
|
230
246
|
contentLocale={contentLocale}
|
|
247
|
+
components={groupAdmin?.[innerField.name]?.components}
|
|
248
|
+
editor={groupAdmin?.[innerField.name]?.editor}
|
|
249
|
+
fieldAdmin={sliceFieldAdmin(groupAdmin, innerField.name)}
|
|
231
250
|
/>
|
|
232
251
|
))}
|
|
233
252
|
</div>
|
|
@@ -243,19 +262,34 @@ export const ArrayField = ({
|
|
|
243
262
|
disableSorting={true}
|
|
244
263
|
collectionPath={collectionPath}
|
|
245
264
|
contentLocale={contentLocale}
|
|
265
|
+
components={fieldAdmin?.[childField.name]?.components}
|
|
266
|
+
editor={fieldAdmin?.[childField.name]?.editor}
|
|
267
|
+
fieldAdmin={sliceFieldAdmin(fieldAdmin, childField.name)}
|
|
246
268
|
/>
|
|
247
269
|
)
|
|
248
270
|
})
|
|
249
271
|
|
|
250
|
-
|
|
272
|
+
// Per-item label: the array label plus a 1-based position, so items
|
|
273
|
+
// read "Questions 1", "Questions 2" rather than all repeating the bare
|
|
274
|
+
// array label.
|
|
275
|
+
const itemLabel = `${field.label ?? field.name} ${index + 1}`
|
|
251
276
|
|
|
277
|
+
// `disableSorting` removes only the drag affordance. Structural editing
|
|
278
|
+
// (add-below / remove / collapse) is always available — StaticItem is
|
|
279
|
+
// the grip-less sibling of SortableItem (see sortable-item.tsx for why
|
|
280
|
+
// it is a separate component rather than a prop).
|
|
252
281
|
if (disableSorting) {
|
|
253
282
|
return (
|
|
254
|
-
<
|
|
283
|
+
<StaticItem
|
|
284
|
+
key={itemWrapper.id}
|
|
285
|
+
label={itemLabel}
|
|
286
|
+
onAddBelow={() => handleInsertBelow(index)}
|
|
287
|
+
onRemove={() => handleRemoveItem(index)}
|
|
288
|
+
>
|
|
255
289
|
<div className={cx('byline-field-array-group-fields', styles['group-fields'])}>
|
|
256
290
|
{innerBody}
|
|
257
291
|
</div>
|
|
258
|
-
</
|
|
292
|
+
</StaticItem>
|
|
259
293
|
)
|
|
260
294
|
}
|
|
261
295
|
|
|
@@ -263,7 +297,7 @@ export const ArrayField = ({
|
|
|
263
297
|
<SortableItem
|
|
264
298
|
key={itemWrapper.id}
|
|
265
299
|
id={itemWrapper.id}
|
|
266
|
-
label={
|
|
300
|
+
label={itemLabel}
|
|
267
301
|
onAddBelow={() => handleInsertBelow(index)}
|
|
268
302
|
onRemove={() => handleRemoveItem(index)}
|
|
269
303
|
>
|
|
@@ -274,14 +308,43 @@ export const ArrayField = ({
|
|
|
274
308
|
)
|
|
275
309
|
}
|
|
276
310
|
|
|
311
|
+
// The add-row renders in both branches — structural editing does not
|
|
312
|
+
// depend on sortability.
|
|
313
|
+
const addRow = (
|
|
314
|
+
<div className={cx('byline-field-array-add-row', styles['add-row'])}>
|
|
315
|
+
<IconButton
|
|
316
|
+
onClick={() => {
|
|
317
|
+
void handleAddItem()
|
|
318
|
+
}}
|
|
319
|
+
aria-label={t('fields.array.addItemAriaLabel')}
|
|
320
|
+
>
|
|
321
|
+
<PlusIcon />
|
|
322
|
+
</IconButton>
|
|
323
|
+
{/* Text-styled button so the label is clickable too. Removed from
|
|
324
|
+
the tab order (tabIndex -1) — the IconButton is the single
|
|
325
|
+
keyboard/screen-reader control for this action. */}
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
tabIndex={-1}
|
|
329
|
+
onClick={() => {
|
|
330
|
+
void handleAddItem()
|
|
331
|
+
}}
|
|
332
|
+
className={cx('byline-field-array-add-label', styles['add-label'])}
|
|
333
|
+
>
|
|
334
|
+
{t('fields.array.addItem')}
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
)
|
|
338
|
+
|
|
277
339
|
return (
|
|
278
340
|
<div className={`byline-field-array ${field.name}`}>
|
|
279
|
-
{
|
|
341
|
+
{field.label && (
|
|
280
342
|
<h3 className={cx('byline-field-array-title', styles.title)}>{field.label}</h3>
|
|
281
343
|
)}
|
|
282
344
|
{disableSorting ? (
|
|
283
345
|
<div className={cx('byline-field-array-stack', styles.stack)}>
|
|
284
346
|
{items.map((item, index) => renderItem(item, index))}
|
|
347
|
+
{addRow}
|
|
285
348
|
</div>
|
|
286
349
|
) : (
|
|
287
350
|
<DraggableSortable
|
|
@@ -290,16 +353,7 @@ export const ArrayField = ({
|
|
|
290
353
|
className={cx('byline-field-array-stack', styles.stack)}
|
|
291
354
|
>
|
|
292
355
|
{items.map((item, index) => renderItem(item, index))}
|
|
293
|
-
|
|
294
|
-
<IconButton
|
|
295
|
-
onClick={() => {
|
|
296
|
-
void handleAddItem()
|
|
297
|
-
}}
|
|
298
|
-
aria-label={t('fields.array.addItemAriaLabel')}
|
|
299
|
-
>
|
|
300
|
-
<PlusIcon />
|
|
301
|
-
</IconButton>
|
|
302
|
-
</span>
|
|
356
|
+
{addRow}
|
|
303
357
|
</DraggableSortable>
|
|
304
358
|
)}
|
|
305
359
|
</div>
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* .byline-field-blocks-title — block-list label heading
|
|
7
7
|
* .byline-field-blocks-stack — vertical stack of blocks + add row
|
|
8
8
|
* .byline-field-blocks-add-row — add-button row at the bottom
|
|
9
|
+
* .byline-field-blocks-add-label — clickable text label in the add row
|
|
9
10
|
* .byline-field-blocks-modal-head — picker modal header
|
|
10
11
|
* .byline-field-blocks-modal-title — picker modal title
|
|
11
12
|
* .byline-field-blocks-grid — grid of variant cards
|
|
@@ -36,9 +37,35 @@
|
|
|
36
37
|
:global(.byline-field-blocks-add-row) {
|
|
37
38
|
display: flex;
|
|
38
39
|
align-items: center;
|
|
40
|
+
font-size: 0.9rem;
|
|
41
|
+
color: var(--muted);
|
|
39
42
|
gap: var(--spacing-8);
|
|
40
43
|
}
|
|
41
44
|
|
|
45
|
+
/* Text-styled companion button for the add row's label — clickable like the
|
|
46
|
+
icon button. Hovering anywhere on the row brightens the label text. */
|
|
47
|
+
.add-label,
|
|
48
|
+
:global(.byline-field-blocks-add-label) {
|
|
49
|
+
appearance: none;
|
|
50
|
+
background: none;
|
|
51
|
+
border: none;
|
|
52
|
+
padding: 0;
|
|
53
|
+
font: inherit;
|
|
54
|
+
color: inherit;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
transition: color 120ms ease;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.add-label:disabled,
|
|
60
|
+
:global(.byline-field-blocks-add-label):disabled {
|
|
61
|
+
cursor: default;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.add-row:hover .add-label:not(:disabled),
|
|
65
|
+
:global(.byline-field-blocks-add-row):hover :global(.byline-field-blocks-add-label):not(:disabled) {
|
|
66
|
+
color: var(--text);
|
|
67
|
+
}
|
|
68
|
+
|
|
42
69
|
.modal-head,
|
|
43
70
|
:global(.byline-field-blocks-modal-head) {
|
|
44
71
|
padding-top: 1rem;
|
|
@@ -54,6 +81,32 @@
|
|
|
54
81
|
.modal-content,
|
|
55
82
|
:global(.byline-field-blocks-card-cursor) {
|
|
56
83
|
cursor: pointer;
|
|
84
|
+
/* Cap the picker's height and scroll the card grid when a collection
|
|
85
|
+
declares more blocks than fit. Mobile-first: 80% of the (dynamic)
|
|
86
|
+
viewport height, tightening to 70% on tablet and 60% on desktop —
|
|
87
|
+
smaller screens get proportionally more of the picker. `min-height: 0`
|
|
88
|
+
lets this flex child (Modal.Content is a flex column item) actually
|
|
89
|
+
shrink and scroll; `overscroll-behavior` keeps wheel momentum from
|
|
90
|
+
spilling into the document behind the modal. All caps stay inside the
|
|
91
|
+
Modal.Container's own viewport-height limit. */
|
|
92
|
+
max-height: 80dvh;
|
|
93
|
+
min-height: 0;
|
|
94
|
+
overflow-y: auto;
|
|
95
|
+
overscroll-behavior: contain;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@media (min-width: 40rem) {
|
|
99
|
+
.modal-content,
|
|
100
|
+
:global(.byline-field-blocks-card-cursor) {
|
|
101
|
+
max-height: 70dvh;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@media (min-width: 64rem) {
|
|
106
|
+
.modal-content,
|
|
107
|
+
:global(.byline-field-blocks-card-cursor) {
|
|
108
|
+
max-height: 60dvh;
|
|
109
|
+
}
|
|
57
110
|
}
|
|
58
111
|
|
|
59
112
|
.grid,
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
import { useEffect, useMemo, useState } from 'react'
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
|
+
BlockAdminConfig,
|
|
12
13
|
BlocksField as BlocksFieldType,
|
|
13
14
|
Field,
|
|
14
15
|
GroupField as GroupFieldType,
|
|
15
16
|
} from '@byline/core'
|
|
17
|
+
import { getClientConfig } from '@byline/core'
|
|
16
18
|
import { useTranslation } from '@byline/i18n/react'
|
|
17
19
|
import {
|
|
18
20
|
Card,
|
|
@@ -59,6 +61,19 @@ export const BlocksField = ({
|
|
|
59
61
|
const [pendingInsertIndex, setPendingInsertIndex] = useState<number | null>(null)
|
|
60
62
|
|
|
61
63
|
const availableBlocks = useMemo(() => field.blocks ?? [], [field.blocks])
|
|
64
|
+
|
|
65
|
+
// Site-wide per-block admin config (`ClientConfig.blockAdmin`), resolved by
|
|
66
|
+
// blockType. Registry-based on purpose: blocks are cross-collection units,
|
|
67
|
+
// so their admin config applies wherever the block renders — no threading
|
|
68
|
+
// from the collection admin config is needed (mirrors how FieldRenderer
|
|
69
|
+
// resolves the global richText editor from client config).
|
|
70
|
+
const blockAdminByType = useMemo(() => {
|
|
71
|
+
const map = new Map<string, BlockAdminConfig>()
|
|
72
|
+
for (const entry of getClientConfig().blockAdmin ?? []) {
|
|
73
|
+
map.set(entry.blockType, entry)
|
|
74
|
+
}
|
|
75
|
+
return map
|
|
76
|
+
}, [])
|
|
62
77
|
const [selectedBlockName, setSelectedBlockName] = useState<string>(
|
|
63
78
|
() => availableBlocks[0]?.blockType ?? ''
|
|
64
79
|
)
|
|
@@ -234,7 +249,13 @@ export const BlocksField = ({
|
|
|
234
249
|
}
|
|
235
250
|
defaultValue={fieldData}
|
|
236
251
|
path={arrayElementPath}
|
|
252
|
+
// Arrays directly inside a block are fully sortable: each
|
|
253
|
+
// DraggableSortable is an independent DndContext and drag listeners
|
|
254
|
+
// are grip-scoped, so the inner array's drags can't leak into the
|
|
255
|
+
// block-level context (and vice versa).
|
|
256
|
+
disableSorting={false}
|
|
237
257
|
contentLocale={contentLocale}
|
|
258
|
+
fieldAdmin={blockAdminByType.get(item._type)?.fields}
|
|
238
259
|
/>
|
|
239
260
|
)
|
|
240
261
|
|
|
@@ -270,9 +291,25 @@ export const BlocksField = ({
|
|
|
270
291
|
}}
|
|
271
292
|
disabled={!selectedBlockName}
|
|
272
293
|
aria-label={t('fields.blocks.addBlockAriaLabel')}
|
|
294
|
+
variant="outlined"
|
|
273
295
|
>
|
|
274
296
|
<PlusIcon />
|
|
275
297
|
</IconButton>
|
|
298
|
+
{/* Text-styled button so the label is clickable too. Removed from
|
|
299
|
+
the tab order (tabIndex -1) — the IconButton is the single
|
|
300
|
+
keyboard/screen-reader control for this action. */}
|
|
301
|
+
<button
|
|
302
|
+
type="button"
|
|
303
|
+
tabIndex={-1}
|
|
304
|
+
disabled={!selectedBlockName}
|
|
305
|
+
onClick={() => {
|
|
306
|
+
setPendingInsertIndex(null)
|
|
307
|
+
setShowAddBlockModal(true)
|
|
308
|
+
}}
|
|
309
|
+
className={cx('byline-field-blocks-add-label', styles['add-label'])}
|
|
310
|
+
>
|
|
311
|
+
{t('fields.blocks.addBlock')}
|
|
312
|
+
</button>
|
|
276
313
|
</div>
|
|
277
314
|
</DraggableSortable>
|
|
278
315
|
<Modal
|
|
@@ -283,7 +320,7 @@ export const BlocksField = ({
|
|
|
283
320
|
setPendingInsertIndex(null)
|
|
284
321
|
}}
|
|
285
322
|
>
|
|
286
|
-
<Modal.Container style={{ maxWidth: '
|
|
323
|
+
<Modal.Container style={{ maxWidth: '700px' }}>
|
|
287
324
|
<Modal.Header className={cx('byline-field-blocks-modal-head', styles['modal-head'])}>
|
|
288
325
|
<h3 className={cx('byline-field-blocks-modal-title', styles['modal-title'])}>
|
|
289
326
|
{t('fields.blocks.modalTitle')}
|