@byline/admin 5.1.4 → 5.2.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.
Files changed (78) hide show
  1. package/package.json +14 -14
  2. package/dist/exports-parity.test.node.js +0 -37
  3. package/dist/fields/array/array-field.jsx +0 -209
  4. package/dist/fields/blocks/blocks-field.jsx +0 -248
  5. package/dist/fields/checkbox/checkbox-field.jsx +0 -24
  6. package/dist/fields/code/code-editor.jsx +0 -211
  7. package/dist/fields/code/code-field.jsx +0 -109
  8. package/dist/fields/column-formatter.jsx +0 -25
  9. package/dist/fields/date-time-formatter.jsx +0 -18
  10. package/dist/fields/datetime/datetime-field.jsx +0 -22
  11. package/dist/fields/draggable-context-menu.jsx +0 -50
  12. package/dist/fields/field-admin.test.node.js +0 -40
  13. package/dist/fields/field-renderer.jsx +0 -136
  14. package/dist/fields/field-services-context.jsx +0 -17
  15. package/dist/fields/file/file-field.jsx +0 -176
  16. package/dist/fields/file/file-upload-field.jsx +0 -89
  17. package/dist/fields/group/group-field.jsx +0 -46
  18. package/dist/fields/image/image-field.jsx +0 -183
  19. package/dist/fields/image/image-upload-field.jsx +0 -157
  20. package/dist/fields/local-date-time.jsx +0 -69
  21. package/dist/fields/locale-badge.jsx +0 -19
  22. package/dist/fields/numerical/numerical-field.jsx +0 -93
  23. package/dist/fields/relation/relation-column-formatter.jsx +0 -74
  24. package/dist/fields/relation/relation-display.jsx +0 -118
  25. package/dist/fields/relation/relation-field.jsx +0 -108
  26. package/dist/fields/relation/relation-many-field.jsx +0 -142
  27. package/dist/fields/relation/relation-picker.jsx +0 -240
  28. package/dist/fields/relation/relation-summary.jsx +0 -45
  29. package/dist/fields/select/select-field.jsx +0 -27
  30. package/dist/fields/sortable-item.jsx +0 -54
  31. package/dist/fields/text/text-field.jsx +0 -79
  32. package/dist/fields/text-area/text-area-field.jsx +0 -79
  33. package/dist/forms/available-locales-reconcile.test.node.js +0 -43
  34. package/dist/forms/available-locales-widget.jsx +0 -63
  35. package/dist/forms/document-actions.jsx +0 -486
  36. package/dist/forms/form-context.jsx +0 -598
  37. package/dist/forms/form-modals.jsx +0 -128
  38. package/dist/forms/form-renderer.jsx +0 -532
  39. package/dist/forms/form-status-display.jsx +0 -69
  40. package/dist/forms/navigation-guard.jsx +0 -68
  41. package/dist/forms/nested-path.test.node.js +0 -115
  42. package/dist/forms/path-widget.jsx +0 -125
  43. package/dist/forms/pending-uploads.test.node.js +0 -19
  44. package/dist/forms/repeating-items.test.node.js +0 -29
  45. package/dist/forms/scheduled-publication-control.jsx +0 -389
  46. package/dist/forms/scheduled-publication-state.test.node.js +0 -121
  47. package/dist/forms/scheduled-publication-time.test.node.js +0 -70
  48. package/dist/forms/status-transitions.test.node.js +0 -76
  49. package/dist/forms/tree-placement-widget.jsx +0 -160
  50. package/dist/forms/upload-executor.test.node.js +0 -368
  51. package/dist/forms/use-form-layout.test.node.js +0 -69
  52. package/dist/modules/admin-account/components/change-password.jsx +0 -169
  53. package/dist/modules/admin-account/components/container.jsx +0 -164
  54. package/dist/modules/admin-account/components/preferences.jsx +0 -148
  55. package/dist/modules/admin-account/components/theme-switch.jsx +0 -52
  56. package/dist/modules/admin-account/components/update.jsx +0 -186
  57. package/dist/modules/admin-permissions/components/inspector.jsx +0 -200
  58. package/dist/modules/admin-preferences/schemas.test.node.js +0 -48
  59. package/dist/modules/admin-roles/components/create.jsx +0 -155
  60. package/dist/modules/admin-roles/components/permissions.jsx +0 -239
  61. package/dist/modules/admin-roles/components/update.jsx +0 -148
  62. package/dist/modules/admin-users/components/create.jsx +0 -206
  63. package/dist/modules/admin-users/components/roles.jsx +0 -126
  64. package/dist/modules/admin-users/components/set-password.jsx +0 -133
  65. package/dist/modules/admin-users/components/update.jsx +0 -215
  66. package/dist/modules/analytics/components/dashboard.jsx +0 -208
  67. package/dist/modules/analytics/components/dashboard.test.node.js +0 -162
  68. package/dist/modules/analytics/components/timeseries.jsx +0 -193
  69. package/dist/modules/auth/components/sign-in-form.jsx +0 -89
  70. package/dist/modules/auth/safe-redirect.test.node.js +0 -40
  71. package/dist/modules/auth/sign-in-form-props.test.node.js +0 -7
  72. package/dist/presentation/group.jsx +0 -26
  73. package/dist/presentation/row.jsx +0 -23
  74. package/dist/presentation/tabs.jsx +0 -39
  75. package/dist/services/admin-services-context.jsx +0 -17
  76. package/dist/widgets/diff-viewer/diff-modal.jsx +0 -110
  77. package/dist/widgets/source-locale-badge/source-locale-badge.jsx +0 -28
  78. package/dist/widgets/status-badge/status-badge.jsx +0 -46
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": "5.1.4",
5
+ "version": "5.2.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -182,29 +182,29 @@
182
182
  "jose": "^6.2.12",
183
183
  "react-diff-viewer-continued": "^4.4.0",
184
184
  "uuid": "^14.0.2",
185
- "zod": "^4.5.4",
186
- "@byline/analytics": "5.1.4",
187
- "@byline/auth": "5.1.4",
188
- "@byline/i18n": "5.1.4",
189
- "@byline/ui": "5.1.4",
190
- "@byline/analytics-agent": "5.1.4",
191
- "@byline/core": "5.1.4"
185
+ "zod": "^4.6.2",
186
+ "@byline/analytics": "5.2.0",
187
+ "@byline/core": "5.2.0",
188
+ "@byline/analytics-agent": "5.2.0",
189
+ "@byline/ui": "5.2.0",
190
+ "@byline/i18n": "5.2.0",
191
+ "@byline/auth": "5.2.0"
192
192
  },
193
193
  "peerDependencies": {
194
194
  "react": "^19.0.0",
195
195
  "react-dom": "^19.0.0"
196
196
  },
197
197
  "devDependencies": {
198
- "@biomejs/biome": "2.5.12",
198
+ "@biomejs/biome": "2.5.13",
199
199
  "@rsbuild/plugin-react": "^2.1.0",
200
200
  "@rslib/core": "^1.0.0",
201
- "@types/node": "^26.5.0",
202
- "@types/react": "19.2.18",
203
- "@types/react-dom": "19.2.7",
201
+ "@types/node": "^26.5.1",
202
+ "@types/react": "19.3.0",
203
+ "@types/react-dom": "19.3.0",
204
204
  "@vitejs/plugin-react": "^6.1.1",
205
205
  "jsdom": "^30.0.1",
206
- "react": "^19.2.8",
207
- "react-dom": "^19.2.8",
206
+ "react": "^19.3.0",
207
+ "react-dom": "^19.3.0",
208
208
  "typescript": "^7.0.2",
209
209
  "typescript-plugin-css-modules": "^5.2.0",
210
210
  "vitest": "^5.0.0"
@@ -1,37 +0,0 @@
1
- /**
2
- * This Source Code is subject to the terms of the Mozilla Public
3
- * License, v. 2.0. If a copy of the MPL was not distributed with this
4
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
- *
6
- * Copyright (c) Infonomic Company Limited
7
- */
8
- /**
9
- * Guard against `publishConfig.exports` drift.
10
- *
11
- * `@byline/admin` carries a `publishConfig.exports` block that npm uses to
12
- * **override** the top-level `exports` at publish time. The workspace and dev
13
- * builds resolve through the top-level `exports` (or source), so a subpath
14
- * added there but forgotten in `publishConfig.exports` typechecks and builds
15
- * locally yet is **missing from the published package** — surfacing only as a
16
- * downstream consumer's build error ("X is not exported …"). That is exactly
17
- * how `./admin-activity` slipped through in v3.11.0.
18
- *
19
- * This test fails the moment the two blocks drift, so the gap is caught in
20
- * `pnpm test` / CI rather than in a production Docker build.
21
- */
22
- import { readFileSync } from 'node:fs';
23
- import { dirname, resolve } from 'node:path';
24
- import { fileURLToPath } from 'node:url';
25
- import { describe, expect, it } from 'vitest';
26
- const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json');
27
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
28
- describe('package.json export parity', () => {
29
- it('every top-level export subpath is also declared in publishConfig.exports', () => {
30
- const publishExports = pkg.publishConfig?.exports;
31
- // If there is no override, the top-level exports ship as-is — nothing to check.
32
- if (publishExports == null)
33
- return;
34
- const missing = Object.keys(pkg.exports).filter((key) => !(key in publishExports));
35
- expect(missing, `publishConfig.exports is missing subpath(s) present in the top-level exports: ${missing.join(', ')}. The published package would not expose them — add them to BOTH blocks.`).toEqual([]);
36
- });
37
- });
@@ -1,209 +0,0 @@
1
- /**
2
- * This Source Code is subject to the terms of the Mozilla Public
3
- * License, v. 2.0. If a copy of the MPL was not distributed with this
4
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
- *
6
- * Copyright (c) Infonomic Company Limited
7
- */
8
- import { useEffect, useState } from 'react';
9
- import { useTranslation } from '@byline/i18n/react';
10
- import { DraggableSortable, IconButton, moveItem, PlusIcon } from '@byline/ui/react';
11
- import cx from 'clsx';
12
- import { sliceFieldAdmin } from '../../fields/field-admin';
13
- import { defaultScalarForField } from '../../fields/field-helpers';
14
- import { FieldRenderer } from '../../fields/field-renderer';
15
- import { SortableItem, StaticItem } from '../../fields/sortable-item';
16
- import { useFormContext } from '../../forms/form-context';
17
- import { hasExistingIdTargets } from '../../forms/nested-path';
18
- import { moveRepeatingItems, repeatingItemId, repeatingItemPath } from '../../forms/repeating-items';
19
- import styles from './array-field.module.css';
20
- // ---------------------------------------------------------------------------
21
- // ArrayField — renders `type: 'array'` fields. Each item renders every child
22
- // field declared in the array's `fields` (value fields and/or groups, in any
23
- // combination). Supports D&D.
24
- // ---------------------------------------------------------------------------
25
- export const ArrayField = ({ field, defaultValue, path, disableSorting = false, contentLocale, fieldAdmin, }) => {
26
- const { appendPatch, getFieldValue, getFieldValues, removePendingUploadsUnder, setFieldStore } = useFormContext();
27
- const { t } = useTranslation('byline-admin');
28
- const [items, setItems] = useState([]);
29
- useEffect(() => {
30
- // Prefer the live form-store value over `defaultValue` when seeding the
31
- // rendered list. The store lives in FormProvider — above the tab layout —
32
- // so it survives this field unmounting/remounting as the editor switches
33
- // tabs; `defaultValue` is only the initial seed and is empty in create
34
- // mode. Re-seeding from `defaultValue` on remount would drop items the
35
- // editor had already added on an earlier visit to this tab.
36
- const storeValue = getFieldValue(path);
37
- const source = Array.isArray(storeValue) ? storeValue : defaultValue;
38
- if (Array.isArray(source)) {
39
- setItems(source.map((item) => ({
40
- id: item && typeof item === 'object' && '_id' in item
41
- ? String(item._id)
42
- : item && typeof item === 'object' && 'id' in item
43
- ? String(item.id)
44
- : crypto.randomUUID(),
45
- data: item,
46
- })));
47
- }
48
- else {
49
- setItems([]);
50
- }
51
- }, [defaultValue, getFieldValue, path]);
52
- const handleDragEnd = ({ moveFromIndex, moveToIndex, }) => {
53
- const currentArray = (getFieldValue(path) ?? defaultValue);
54
- if (!Array.isArray(currentArray))
55
- return;
56
- const move = moveRepeatingItems(currentArray, moveFromIndex, moveToIndex);
57
- if (move == null)
58
- return;
59
- setItems((prev) => moveItem(prev, move.fromIndex, move.toIndex));
60
- setFieldStore(path, move.items);
61
- appendPatch({
62
- kind: 'array.move',
63
- path,
64
- itemId: move.itemId,
65
- toIndex: move.toIndex,
66
- });
67
- };
68
- const handleAddItem = async (atIndex) => {
69
- const childFields = field.fields ?? [];
70
- if (childFields.length === 0)
71
- return;
72
- const newId = crypto.randomUUID();
73
- // Build a new item with default values for ALL child fields.
74
- // Assign the stable `_id` client-side — exactly like BlocksField's
75
- // handleAddItem — so the item is patch-addressable (move / remove by
76
- // `_id`) within the same editing session, before any server write has
77
- // assigned identity. The storage layer persists `_id` via store_meta.
78
- const newItem = { _id: newId };
79
- for (const childField of childFields) {
80
- if (childField.type === 'group' && childField.fields && childField.fields.length > 0) {
81
- // Group child — build a nested object with defaults for each inner field
82
- const groupObj = {};
83
- for (const innerField of childField.fields) {
84
- groupObj[innerField.name] = await defaultScalarForField(innerField, getFieldValues);
85
- }
86
- newItem[childField.name] = groupObj;
87
- }
88
- else {
89
- newItem[childField.name] = await defaultScalarForField(childField, getFieldValues);
90
- }
91
- }
92
- // Defaults may resolve asynchronously. If an enclosing stable-id item was
93
- // removed in the meantime, do not append a structural patch that would
94
- // recreate that missing parent on the server.
95
- if (!hasExistingIdTargets(getFieldValues(), path))
96
- return;
97
- const currentArray = (getFieldValue(path) ?? defaultValue);
98
- const insertAt = atIndex != null ? atIndex : currentArray ? currentArray.length : 0;
99
- const newItemWrapper = { id: newId, data: newItem };
100
- setItems((prev) => {
101
- const next = [...prev];
102
- next.splice(insertAt, 0, newItemWrapper);
103
- return next;
104
- });
105
- appendPatch({
106
- kind: 'array.insert',
107
- path: path,
108
- index: insertAt,
109
- item: newItem,
110
- });
111
- const newArrayValue = currentArray ? [...currentArray] : [];
112
- newArrayValue.splice(insertAt, 0, newItem);
113
- setFieldStore(path, newArrayValue);
114
- };
115
- const handleRemoveItem = (index) => {
116
- const currentArray = (getFieldValue(path) ?? defaultValue);
117
- if (!Array.isArray(currentArray) || index < 0 || index >= currentArray.length)
118
- return;
119
- const item = currentArray[index];
120
- const itemPath = repeatingItemPath(path, item, index);
121
- setItems((prev) => prev.filter((_, i) => i !== index));
122
- removePendingUploadsUnder(itemPath);
123
- appendPatch({
124
- kind: 'array.remove',
125
- path: path,
126
- itemId: repeatingItemId(item) ?? String(index),
127
- });
128
- const newArrayValue = [...currentArray];
129
- newArrayValue.splice(index, 1);
130
- setFieldStore(path, newArrayValue);
131
- };
132
- const handleInsertBelow = (index) => {
133
- void handleAddItem(index + 1);
134
- };
135
- const renderItem = (itemWrapper, index) => {
136
- const item = itemWrapper.data;
137
- const arrayElementPath = repeatingItemPath(path, item, index);
138
- if (!item || typeof item !== 'object')
139
- return null;
140
- const childFields = field.fields ?? [];
141
- if (childFields.length === 0)
142
- return null;
143
- // Render ALL child fields defined in the array's field schema
144
- const innerBody = childFields.map((childField) => {
145
- const initial = item[childField.name];
146
- if (childField.type === 'group' && childField.fields && childField.fields.length > 0) {
147
- // Group child — render its inner fields with the group's sub-object.
148
- // fieldAdmin slices one level per structural hop: the group's
149
- // descendant map first, then each inner field's own slice.
150
- const groupData = initial && typeof initial === 'object' ? initial : {};
151
- const groupAdmin = sliceFieldAdmin(fieldAdmin, childField.name);
152
- return (<div key={childField.name} className={cx('byline-field-array-group-fields', styles['group-fields'])}>
153
- {childField.label && (<h4 className={cx('byline-field-array-group-header', styles['group-header'])}>
154
- {childField.label}
155
- </h4>)}
156
- {childField.fields.map((innerField) => (<FieldRenderer key={innerField.name} field={innerField} defaultValue={groupData[innerField.name]} basePath={`${arrayElementPath}.${childField.name}`} disableSorting={true} contentLocale={contentLocale} components={groupAdmin?.[innerField.name]?.components} editor={groupAdmin?.[innerField.name]?.editor} fieldAdmin={sliceFieldAdmin(groupAdmin, innerField.name)}/>))}
157
- </div>);
158
- }
159
- return (<FieldRenderer key={childField.name} field={childField} defaultValue={initial} basePath={arrayElementPath} disableSorting={true} contentLocale={contentLocale} components={fieldAdmin?.[childField.name]?.components} editor={fieldAdmin?.[childField.name]?.editor} fieldAdmin={sliceFieldAdmin(fieldAdmin, childField.name)}/>);
160
- });
161
- // Per-item label: the array label plus a 1-based position, so items
162
- // read "Questions 1", "Questions 2" rather than all repeating the bare
163
- // array label.
164
- const itemLabel = `${field.label ?? field.name} ${index + 1}`;
165
- // `disableSorting` removes only the drag affordance. Structural editing
166
- // (add-below / remove / collapse) is always available — StaticItem is
167
- // the grip-less sibling of SortableItem (see sortable-item.tsx for why
168
- // it is a separate component rather than a prop).
169
- if (disableSorting) {
170
- return (<StaticItem key={itemWrapper.id} label={itemLabel} onAddBelow={() => handleInsertBelow(index)} onRemove={() => handleRemoveItem(index)}>
171
- <div className={cx('byline-field-array-group-fields', styles['group-fields'])}>
172
- {innerBody}
173
- </div>
174
- </StaticItem>);
175
- }
176
- return (<SortableItem key={itemWrapper.id} id={itemWrapper.id} label={itemLabel} onAddBelow={() => handleInsertBelow(index)} onRemove={() => handleRemoveItem(index)}>
177
- <div className={cx('byline-field-array-group-fields', styles['group-fields'])}>
178
- {innerBody}
179
- </div>
180
- </SortableItem>);
181
- };
182
- // The add-row renders in both branches — structural editing does not
183
- // depend on sortability.
184
- const addRow = (<div className={cx('byline-field-array-add-row', styles['add-row'])}>
185
- <IconButton onClick={() => {
186
- void handleAddItem();
187
- }} aria-label={t('fields.array.addItemAriaLabel')}>
188
- <PlusIcon />
189
- </IconButton>
190
- {/* Text-styled button so the label is clickable too. Removed from
191
- the tab order (tabIndex -1) — the IconButton is the single
192
- keyboard/screen-reader control for this action. */}
193
- <button type="button" tabIndex={-1} onClick={() => {
194
- void handleAddItem();
195
- }} className={cx('byline-field-array-add-label', styles['add-label'])}>
196
- {t('fields.array.addItem')}
197
- </button>
198
- </div>);
199
- return (<div className={`byline-field-array ${field.name}`}>
200
- {field.label && (<h3 className={cx('byline-field-array-title', styles.title)}>{field.label}</h3>)}
201
- {disableSorting ? (<div className={cx('byline-field-array-stack', styles.stack)}>
202
- {items.map((item, index) => renderItem(item, index))}
203
- {addRow}
204
- </div>) : (<DraggableSortable ids={items.map((i) => i.id)} onDragEnd={handleDragEnd} className={cx('byline-field-array-stack', styles.stack)}>
205
- {items.map((item, index) => renderItem(item, index))}
206
- {addRow}
207
- </DraggableSortable>)}
208
- </div>);
209
- };
@@ -1,248 +0,0 @@
1
- /**
2
- * This Source Code is subject to the terms of the Mozilla Public
3
- * License, v. 2.0. If a copy of the MPL was not distributed with this
4
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
- *
6
- * Copyright (c) Infonomic Company Limited
7
- */
8
- import { useEffect, useMemo, useState } from 'react';
9
- import { getAdminConfig } from '@byline/core';
10
- import { useTranslation } from '@byline/i18n/react';
11
- import { Card, CloseIcon, DraggableSortable, IconButton, Modal, moveItem, PlusIcon, } from '@byline/ui/react';
12
- import cx from 'clsx';
13
- import { defaultScalarForField } from '../../fields/field-helpers';
14
- import { GroupField } from '../../fields/group/group-field';
15
- import { SortableItem } from '../../fields/sortable-item';
16
- import { useFormContext } from '../../forms/form-context';
17
- import { hasExistingIdTargets } from '../../forms/nested-path';
18
- import { moveRepeatingItems, repeatingItemId, repeatingItemPath } from '../../forms/repeating-items';
19
- import styles from './blocks-field.module.css';
20
- // ---------------------------------------------------------------------------
21
- // BlocksField — renders `type: 'blocks'` fields. Children are heterogeneous
22
- // group fields selected via a modal picker. Supports D&D.
23
- // ---------------------------------------------------------------------------
24
- export const BlocksField = ({ field, defaultValue, path, contentLocale, }) => {
25
- const { appendPatch, getFieldValue, getFieldValues, removePendingUploadsUnder, setFieldStore } = useFormContext();
26
- const { t } = useTranslation('byline-admin');
27
- const [items, setItems] = useState([]);
28
- const [showAddBlockModal, setShowAddBlockModal] = useState(false);
29
- const [pendingInsertIndex, setPendingInsertIndex] = useState(null);
30
- const availableBlocks = useMemo(() => field.blocks ?? [], [field.blocks]);
31
- // Site-wide per-block admin config (`AdminConfig.blockAdmin`), resolved by
32
- // blockType. Registry-based on purpose: blocks are cross-collection units,
33
- // so their admin config applies wherever the block renders — no threading
34
- // from the collection admin config is needed (mirrors how FieldRenderer
35
- // resolves the global richText editor from admin config).
36
- const blockAdminByType = useMemo(() => {
37
- const map = new Map();
38
- for (const entry of getAdminConfig().blockAdmin ?? []) {
39
- map.set(entry.blockType, entry);
40
- }
41
- return map;
42
- }, []);
43
- const [selectedBlockName, setSelectedBlockName] = useState(() => availableBlocks[0]?.blockType ?? '');
44
- useEffect(() => {
45
- if (selectedBlockName == null ||
46
- !availableBlocks.some((b) => b.blockType === selectedBlockName)) {
47
- setSelectedBlockName(availableBlocks[0]?.blockType ?? '');
48
- }
49
- }, [availableBlocks, selectedBlockName]);
50
- useEffect(() => {
51
- // Prefer the live form-store value over `defaultValue` when seeding the
52
- // rendered list. The store lives in FormProvider — above the tab layout —
53
- // so it survives this field unmounting/remounting as the editor switches
54
- // tabs; `defaultValue` is only the initial seed and is empty in create
55
- // mode. Re-seeding from `defaultValue` on remount would drop blocks the
56
- // editor had already added on an earlier visit to this tab.
57
- const storeValue = getFieldValue(path);
58
- const source = Array.isArray(storeValue) ? storeValue : defaultValue;
59
- if (Array.isArray(source)) {
60
- setItems(source.map((item) => ({
61
- id: item && typeof item === 'object' && '_id' in item
62
- ? String(item._id)
63
- : crypto.randomUUID(),
64
- data: item,
65
- })));
66
- }
67
- else {
68
- setItems([]);
69
- }
70
- }, [defaultValue, getFieldValue, path]);
71
- const handleDragEnd = ({ moveFromIndex, moveToIndex, }) => {
72
- const currentArray = (getFieldValue(path) ?? defaultValue);
73
- if (!Array.isArray(currentArray))
74
- return;
75
- const move = moveRepeatingItems(currentArray, moveFromIndex, moveToIndex);
76
- if (move == null)
77
- return;
78
- setItems((prev) => moveItem(prev, move.fromIndex, move.toIndex));
79
- setFieldStore(path, move.items);
80
- appendPatch({
81
- kind: 'array.move',
82
- path,
83
- itemId: move.itemId,
84
- toIndex: move.toIndex,
85
- });
86
- };
87
- const handleAddItem = async (forcedVariantName, atIndex) => {
88
- setShowAddBlockModal(false);
89
- setPendingInsertIndex(null);
90
- const variant = (forcedVariantName != null
91
- ? availableBlocks.find((v) => v.blockType === forcedVariantName)
92
- : undefined) ?? availableBlocks[0];
93
- if (!variant)
94
- return;
95
- const compositeFields = (variant.fields ?? []);
96
- const newId = crypto.randomUUID();
97
- const newItem = {
98
- _id: newId,
99
- _type: variant.blockType,
100
- };
101
- for (const f of compositeFields) {
102
- newItem[f.name] = await defaultScalarForField(f, getFieldValues);
103
- }
104
- if (!hasExistingIdTargets(getFieldValues(), path))
105
- return;
106
- const currentArray = (getFieldValue(path) ?? defaultValue);
107
- const insertAt = atIndex != null ? atIndex : currentArray ? currentArray.length : 0;
108
- const newItemWrapper = { id: newId, data: newItem };
109
- setItems((prev) => {
110
- const next = [...prev];
111
- next.splice(insertAt, 0, newItemWrapper);
112
- return next;
113
- });
114
- appendPatch({
115
- kind: 'array.insert',
116
- path: path,
117
- index: insertAt,
118
- item: newItem,
119
- });
120
- const newArrayValue = currentArray ? [...currentArray] : [];
121
- newArrayValue.splice(insertAt, 0, newItem);
122
- setFieldStore(path, newArrayValue);
123
- };
124
- const handleRemoveItem = (index) => {
125
- const currentArray = (getFieldValue(path) ?? defaultValue);
126
- if (!Array.isArray(currentArray) || index < 0 || index >= currentArray.length)
127
- return;
128
- const item = currentArray[index];
129
- const itemPath = repeatingItemPath(path, item, index);
130
- const itemId = repeatingItemId(item) ?? String(index);
131
- setItems((prev) => prev.filter((_, i) => i !== index));
132
- removePendingUploadsUnder(itemPath);
133
- appendPatch({
134
- kind: 'array.remove',
135
- path: path,
136
- itemId,
137
- });
138
- const newArrayValue = [...currentArray];
139
- newArrayValue.splice(index, 1);
140
- setFieldStore(path, newArrayValue);
141
- };
142
- const handleInsertBelow = (index, forcedVariantName) => {
143
- if (availableBlocks.length > 1 && forcedVariantName == null) {
144
- setPendingInsertIndex(index + 1);
145
- setShowAddBlockModal(true);
146
- }
147
- else {
148
- void handleAddItem(forcedVariantName, index + 1);
149
- }
150
- };
151
- const renderItem = (itemWrapper, index) => {
152
- const item = itemWrapper.data;
153
- const arrayElementPath = repeatingItemPath(path, item, index);
154
- if (!item || typeof item !== 'object' || typeof item._type !== 'string')
155
- return null;
156
- const subField = field.blocks?.find((b) => b.blockType === item._type);
157
- if (subField == null)
158
- return null;
159
- // Extract field data (everything except _id and _type)
160
- const { _id, _type, ...fieldData } = item;
161
- const label = subField.label ?? _type;
162
- // Render the block's children directly with arrayElementPath as the
163
- // path (not basePath). FieldRenderer would append the group name
164
- // (e.g. "richTextBlock") producing paths like
165
- // "content[id=...].richTextBlock.constrainedWidth", but the flat block
166
- // shape stores fields directly on the item so the correct path is
167
- // "content[id=...].constrainedWidth".
168
- const body = (<GroupField key={subField.blockType} field={{
169
- type: 'group',
170
- name: subField.blockType,
171
- fields: subField.fields,
172
- label: undefined,
173
- }} defaultValue={fieldData} path={arrayElementPath}
174
- // Arrays directly inside a block are fully sortable: each
175
- // DraggableSortable is an independent DndContext and drag listeners
176
- // are grip-scoped, so the inner array's drags can't leak into the
177
- // block-level context (and vice versa).
178
- disableSorting={false} contentLocale={contentLocale} fieldAdmin={blockAdminByType.get(item._type)?.fields}/>);
179
- return (<SortableItem key={itemWrapper.id} id={itemWrapper.id} label={label ?? subField.blockType} onAddBelow={() => handleInsertBelow(index)} onRemove={() => handleRemoveItem(index)}>
180
- {body}
181
- </SortableItem>);
182
- };
183
- return (<div className={`byline-field-blocks ${field.name}`}>
184
- {field.label && (<h3 className={cx('byline-field-blocks-title', styles.title)}>{field.label}</h3>)}
185
- <DraggableSortable ids={items.map((i) => i.id)} onDragEnd={handleDragEnd} className={cx('byline-field-blocks-stack', styles.stack)}>
186
- {items.map((item, index) => renderItem(item, index))}
187
- <div className={cx('byline-field-blocks-add-row', styles['add-row'])}>
188
- <IconButton onClick={() => {
189
- setPendingInsertIndex(null);
190
- setShowAddBlockModal(true);
191
- }} disabled={!selectedBlockName} aria-label={t('fields.blocks.addBlockAriaLabel')} variant="outlined">
192
- <PlusIcon />
193
- </IconButton>
194
- {/* Text-styled button so the label is clickable too. Removed from
195
- the tab order (tabIndex -1) — the IconButton is the single
196
- keyboard/screen-reader control for this action. */}
197
- <button type="button" tabIndex={-1} disabled={!selectedBlockName} onClick={() => {
198
- setPendingInsertIndex(null);
199
- setShowAddBlockModal(true);
200
- }} className={cx('byline-field-blocks-add-label', styles['add-label'])}>
201
- {t('fields.blocks.addBlock')}
202
- </button>
203
- </div>
204
- </DraggableSortable>
205
- <Modal isOpen={showAddBlockModal} closeOnOverlayClick={true} onDismiss={() => {
206
- setShowAddBlockModal(false);
207
- setPendingInsertIndex(null);
208
- }}>
209
- <Modal.Container style={{ maxWidth: '700px' }}>
210
- <Modal.Header className={cx('byline-field-blocks-modal-head', styles['modal-head'])}>
211
- <h3 className={cx('byline-field-blocks-modal-title', styles['modal-title'])}>
212
- {t('fields.blocks.modalTitle')}
213
- </h3>
214
- <IconButton arial-label={t('common.actions.close')} size="xs" onClick={() => {
215
- setShowAddBlockModal(false);
216
- setPendingInsertIndex(null);
217
- }}>
218
- <CloseIcon width="16px" height="16px" svgClassName="white-icon"/>
219
- </IconButton>
220
- </Modal.Header>
221
- <Modal.Content className={cx('byline-field-blocks-card-cursor', styles['modal-content'])}>
222
- <div className={cx('byline-field-blocks-grid', styles.grid)}>
223
- {availableBlocks.map((b, index) => (<Card key={b.blockType} hover onClick={() => void handleAddItem(b.blockType, pendingInsertIndex ?? undefined)} className={cx('byline-field-blocks-card', styles.card)}>
224
- <Card.Header>
225
- <div className={cx('byline-field-blocks-card-head', styles['card-head'])}>
226
- <Card.Title className={cx('byline-field-blocks-card-title', styles['card-title'])}>
227
- {b.label ?? b.blockType}
228
- </Card.Title>
229
- <span className={cx('byline-field-blocks-card-index', styles['card-index'])}>
230
- {index + 1}
231
- </span>
232
- </div>
233
- <code className={cx('byline-field-blocks-card-code', styles['card-code'])}>
234
- {b.blockType}
235
- </code>
236
- </Card.Header>
237
- <Card.Content>
238
- <p className={cx('byline-field-blocks-card-body', styles['card-body'])}>
239
- {b.helpText ?? b.label ?? b.blockType}
240
- </p>
241
- </Card.Content>
242
- </Card>))}
243
- </div>
244
- </Modal.Content>
245
- </Modal.Container>
246
- </Modal>
247
- </div>);
248
- };
@@ -1,24 +0,0 @@
1
- /**
2
- * This Source Code is subject to the terms of the Mozilla Public
3
- * License, v. 2.0. If a copy of the MPL was not distributed with this
4
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
- *
6
- * Copyright (c) Infonomic Company Limited
7
- */
8
- import { Checkbox } from '@byline/ui/react';
9
- import { useFieldError, useFieldValue } from '../../forms/form-context';
10
- import styles from './checkbox-field.module.css';
11
- export const CheckboxField = ({ field, value, defaultValue, onChange, id, path, }) => {
12
- const fieldPath = path ?? field.name;
13
- const fieldError = useFieldError(fieldPath);
14
- const fieldValue = useFieldValue(fieldPath);
15
- const checked = value ?? fieldValue ?? defaultValue ?? false;
16
- return (<div className={`byline-field-checkbox ${field.name} ${styles['field-checkbox']}`}>
17
- <Checkbox id={id ?? fieldPath} name={field.name} label={field.label} checked={checked} size="sm" helpText={field.helpText}
18
- // TODO: Handle indeterminate state
19
- onCheckedChange={(value) => {
20
- const next = value === 'indeterminate' ? false : Boolean(value);
21
- onChange?.(next);
22
- }} error={fieldError != null} errorText={fieldError}/>
23
- </div>);
24
- };