@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.
Files changed (58) hide show
  1. package/dist/fields/array/array-field.d.ts +12 -2
  2. package/dist/fields/array/array-field.js +46 -20
  3. package/dist/fields/array/array-field.module.js +5 -1
  4. package/dist/fields/array/array-field_module.css +23 -0
  5. package/dist/fields/blocks/blocks-field.js +34 -12
  6. package/dist/fields/blocks/blocks-field.module.js +2 -0
  7. package/dist/fields/blocks/blocks-field_module.css +37 -0
  8. package/dist/fields/code/code-editor.d.ts +20 -0
  9. package/dist/fields/code/code-editor.js +239 -0
  10. package/dist/fields/code/code-field.d.ts +21 -0
  11. package/dist/fields/code/code-field.js +124 -0
  12. package/dist/fields/code/code-field.module.js +7 -0
  13. package/dist/fields/code/code-field_module.css +58 -0
  14. package/dist/fields/draggable-context-menu.js +2 -1
  15. package/dist/fields/field-admin.d.ts +15 -0
  16. package/dist/fields/field-admin.js +11 -0
  17. package/dist/fields/field-admin.test.node.d.ts +8 -0
  18. package/dist/fields/field-helpers.js +1 -0
  19. package/dist/fields/field-renderer.d.ts +11 -2
  20. package/dist/fields/field-renderer.js +21 -4
  21. package/dist/fields/group/group-field.d.ts +23 -2
  22. package/dist/fields/group/group-field.js +7 -3
  23. package/dist/fields/relation/relation-picker.js +8 -2
  24. package/dist/fields/select/select-field.js +4 -2
  25. package/dist/fields/select/select-field.module.js +1 -0
  26. package/dist/fields/select/select-field_module.css +4 -0
  27. package/dist/fields/sortable-item.d.ts +6 -0
  28. package/dist/fields/sortable-item.js +44 -25
  29. package/dist/fields/text/text-field_module.css +1 -0
  30. package/dist/fields/text-area/text-area-field_module.css +1 -0
  31. package/dist/forms/form-context.js +1 -1
  32. package/dist/forms/form-renderer.d.ts +1 -1
  33. package/dist/forms/form-renderer.js +3 -1
  34. package/dist/forms/tree-placement-widget.d.ts +1 -1
  35. package/package.json +23 -10
  36. package/src/fields/array/array-field.module.css +30 -0
  37. package/src/fields/array/array-field.tsx +74 -20
  38. package/src/fields/blocks/blocks-field.module.css +53 -0
  39. package/src/fields/blocks/blocks-field.tsx +38 -1
  40. package/src/fields/code/code-editor.tsx +246 -0
  41. package/src/fields/code/code-field.module.css +84 -0
  42. package/src/fields/code/code-field.tsx +191 -0
  43. package/src/fields/draggable-context-menu.tsx +9 -1
  44. package/src/fields/field-admin.test.node.ts +49 -0
  45. package/src/fields/field-admin.ts +39 -0
  46. package/src/fields/field-helpers.ts +1 -0
  47. package/src/fields/field-renderer.tsx +33 -2
  48. package/src/fields/field-services-types.ts +1 -1
  49. package/src/fields/group/group-field.tsx +29 -2
  50. package/src/fields/relation/relation-picker.tsx +11 -0
  51. package/src/fields/select/select-field.module.css +5 -0
  52. package/src/fields/select/select-field.tsx +9 -2
  53. package/src/fields/sortable-item.tsx +115 -34
  54. package/src/fields/text/text-field.module.css +1 -0
  55. package/src/fields/text-area/text-area-field.module.css +1 -0
  56. package/src/forms/form-context.tsx +9 -1
  57. package/src/forms/form-renderer.tsx +3 -1
  58. package/src/forms/tree-placement-widget.tsx +1 -1
@@ -0,0 +1,124 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import react, { Suspense, useCallback } from "react";
3
+ import { ErrorText, HelpText, Label } from "@byline/ui/react";
4
+ import classnames from "classnames";
5
+ import { useFieldError, useFieldValue } from "../../forms/form-context.js";
6
+ import { LocaleBadge } from "../locale-badge.js";
7
+ import code_field_module from "./code-field.module.js";
8
+ const CodeEditor = /*#__PURE__*/ react.lazy(()=>import("./code-editor.js"));
9
+ const siblingFieldPath = (fieldPath, siblingName)=>{
10
+ const lastDot = fieldPath.lastIndexOf('.');
11
+ return -1 === lastDot ? siblingName : `${fieldPath.slice(0, lastDot + 1)}${siblingName}`;
12
+ };
13
+ const CodeField = ({ field, value, defaultValue, onChange, id, path, locale, components })=>{
14
+ const fieldPath = path ?? field.name;
15
+ const fieldError = useFieldError(fieldPath);
16
+ const fieldValue = useFieldValue(fieldPath);
17
+ const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
18
+ const htmlId = id ?? fieldPath;
19
+ const siblingPath = field.languageField ? siblingFieldPath(fieldPath, field.languageField) : fieldPath;
20
+ const siblingLanguage = useFieldValue(siblingPath);
21
+ const effectiveLanguage = (field.languageField ? siblingLanguage : void 0) || field.language;
22
+ const handleChange = useCallback((value)=>{
23
+ if (onChange) onChange(value);
24
+ }, [
25
+ onChange
26
+ ]);
27
+ const slots = components;
28
+ const CustomLabel = slots?.Label;
29
+ const CustomHelpText = slots?.HelpText;
30
+ const CustomField = slots?.Field;
31
+ const BeforeField = slots?.beforeField;
32
+ const AfterField = slots?.afterField;
33
+ const slotBaseProps = {
34
+ field: field,
35
+ path: fieldPath,
36
+ value: incomingValue,
37
+ error: fieldError,
38
+ id: htmlId
39
+ };
40
+ const showBadge = !!locale && !!field.label;
41
+ const hasCustomLabel = !!CustomLabel;
42
+ const labelRowClass = classnames('byline-field-code-label-row', code_field_module["label-row"]);
43
+ const renderLabel = ()=>{
44
+ if (hasCustomLabel) return /*#__PURE__*/ jsxs("div", {
45
+ className: labelRowClass,
46
+ children: [
47
+ /*#__PURE__*/ jsx(CustomLabel, {
48
+ ...slotBaseProps,
49
+ label: field.label,
50
+ required: !field.optional
51
+ }),
52
+ showBadge && /*#__PURE__*/ jsx(LocaleBadge, {
53
+ locale: locale
54
+ })
55
+ ]
56
+ });
57
+ if (field.label) return /*#__PURE__*/ jsxs("div", {
58
+ className: labelRowClass,
59
+ children: [
60
+ /*#__PURE__*/ jsx(Label, {
61
+ id: `${htmlId}-label`,
62
+ htmlFor: htmlId,
63
+ label: field.label,
64
+ required: !field.optional
65
+ }),
66
+ showBadge && /*#__PURE__*/ jsx(LocaleBadge, {
67
+ locale: locale
68
+ })
69
+ ]
70
+ });
71
+ return null;
72
+ };
73
+ const renderInput = ()=>{
74
+ if (CustomField) return /*#__PURE__*/ jsx(CustomField, {
75
+ ...slotBaseProps,
76
+ onChange: handleChange,
77
+ defaultValue: defaultValue,
78
+ placeholder: field.placeholder
79
+ });
80
+ return /*#__PURE__*/ jsx(Suspense, {
81
+ fallback: /*#__PURE__*/ jsx("textarea", {
82
+ className: classnames('byline-field-code-loading', code_field_module.loading),
83
+ readOnly: true,
84
+ value: incomingValue,
85
+ rows: 6,
86
+ "aria-label": field.label
87
+ }),
88
+ children: /*#__PURE__*/ jsx(CodeEditor, {
89
+ id: htmlId,
90
+ value: incomingValue,
91
+ language: effectiveLanguage,
92
+ onChange: handleChange,
93
+ readOnly: true === field.readOnly,
94
+ ariaInvalid: null != fieldError,
95
+ ariaDescribedBy: null != fieldError ? `error-for-${htmlId}` : field.helpText ? `help-for-${htmlId}` : void 0
96
+ })
97
+ });
98
+ };
99
+ return /*#__PURE__*/ jsxs("div", {
100
+ className: `byline-field-code ${field.name}`,
101
+ children: [
102
+ renderLabel(),
103
+ BeforeField && /*#__PURE__*/ jsx(BeforeField, {
104
+ ...slotBaseProps
105
+ }),
106
+ renderInput(),
107
+ AfterField && /*#__PURE__*/ jsx(AfterField, {
108
+ ...slotBaseProps
109
+ }),
110
+ null != fieldError && /*#__PURE__*/ jsx(ErrorText, {
111
+ id: `error-for-${htmlId}`,
112
+ text: fieldError
113
+ }),
114
+ CustomHelpText ? /*#__PURE__*/ jsx(CustomHelpText, {
115
+ ...slotBaseProps,
116
+ helpText: field.helpText
117
+ }) : null == fieldError && field.helpText && /*#__PURE__*/ jsx(HelpText, {
118
+ id: `help-for-${htmlId}`,
119
+ text: field.helpText
120
+ })
121
+ ]
122
+ });
123
+ };
124
+ export { CodeField };
@@ -0,0 +1,7 @@
1
+ import "./code-field_module.css";
2
+ const code_field_module = {
3
+ "label-row": "label-row-bNilI8",
4
+ labelRow: "label-row-bNilI8",
5
+ loading: "loading-sejBT6"
6
+ };
7
+ export default code_field_module;
@@ -0,0 +1,58 @@
1
+ :is(.label-row-bNilI8, .byline-field-code-label-row) {
2
+ align-items: center;
3
+ margin-bottom: .25rem;
4
+ display: flex;
5
+ }
6
+
7
+ .byline-field-code {
8
+ --byline-code-bg: var(--canvas-50, #fafafa);
9
+ --byline-code-fg: var(--canvas-950, #1c1c1e);
10
+ --byline-code-border: var(--canvas-300, #d6d6d6);
11
+ --byline-code-gutter-bg: var(--canvas-100, #f2f2f2);
12
+ --byline-code-gutter-fg: var(--canvas-500, #8b8b90);
13
+ --byline-code-active-line: #0000000a;
14
+ --byline-code-selection: #3b82f62e;
15
+ --byline-code-caret: var(--canvas-950, #1c1c1e);
16
+ --byline-code-focus-ring: #3b82f699;
17
+ --byline-code-keyword: #7c3aed;
18
+ --byline-code-string: #15803d;
19
+ --byline-code-comment: #767680;
20
+ --byline-code-number: #c2410c;
21
+ --byline-code-function: #1d4ed8;
22
+ --byline-code-type: #0f766e;
23
+ --byline-code-property: #a16207;
24
+ --byline-code-operator: #52525b;
25
+ }
26
+
27
+ :is([data-theme="dark"], .dark) .byline-field-code {
28
+ --byline-code-bg: var(--canvas-900, #1b1b1f);
29
+ --byline-code-fg: var(--canvas-100, #e6e6ea);
30
+ --byline-code-border: var(--canvas-700, #3f3f46);
31
+ --byline-code-gutter-bg: var(--canvas-800, #232327);
32
+ --byline-code-gutter-fg: var(--canvas-500, #77777f);
33
+ --byline-code-active-line: #ffffff0d;
34
+ --byline-code-selection: #60a5fa47;
35
+ --byline-code-caret: var(--canvas-100, #e6e6ea);
36
+ --byline-code-focus-ring: #60a5fa8c;
37
+ --byline-code-keyword: #c4b5fd;
38
+ --byline-code-string: #86efac;
39
+ --byline-code-comment: #8e8e96;
40
+ --byline-code-number: #fdba74;
41
+ --byline-code-function: #93c5fd;
42
+ --byline-code-type: #5eead4;
43
+ --byline-code-property: #fde047;
44
+ --byline-code-operator: #a1a1aa;
45
+ }
46
+
47
+ :is(.loading-sejBT6, .byline-field-code-loading) {
48
+ resize: none;
49
+ width: 100%;
50
+ font-family: var(--byline-code-font, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
51
+ font-size: var(--byline-code-font-size, 13px);
52
+ color: var(--byline-code-fg);
53
+ background-color: var(--byline-code-bg);
54
+ border: 1px solid var(--byline-code-border);
55
+ border-radius: var(--byline-code-radius, 4px);
56
+ padding: .5rem;
57
+ }
58
+
@@ -16,7 +16,8 @@ function DraggableContextMenu({ onAddBelow, onRemove }) {
16
16
  /*#__PURE__*/ jsx(Dropdown.Trigger, {
17
17
  render: /*#__PURE__*/ jsx(IconButton, {
18
18
  variant: "text",
19
- size: "sm"
19
+ size: "sm",
20
+ "aria-label": t('fields.draggableMenu.triggerAriaLabel')
20
21
  }),
21
22
  children: /*#__PURE__*/ jsx(EllipsisIcon, {
22
23
  width: "16px",
@@ -0,0 +1,15 @@
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 type { FieldAdminConfig } from '@byline/core';
9
+ /**
10
+ * Entries of `map` addressing descendants of `childName`, re-keyed with the
11
+ * `childName.` prefix stripped — the sub-map a structural child (group /
12
+ * array) threads to its own children. Returns `undefined` when `map` has no
13
+ * descendant entries for the child, so leaf widgets aren't handed empty maps.
14
+ */
15
+ export declare function sliceFieldAdmin(map: Record<string, FieldAdminConfig> | undefined, childName: string): Record<string, FieldAdminConfig> | undefined;
@@ -0,0 +1,11 @@
1
+ function sliceFieldAdmin(map, childName) {
2
+ if (null == map) return;
3
+ const prefix = `${childName}.`;
4
+ let sliced;
5
+ for (const [key, value] of Object.entries(map))if (key.startsWith(prefix)) {
6
+ sliced ??= {};
7
+ sliced[key.slice(prefix.length)] = value;
8
+ }
9
+ return sliced;
10
+ }
11
+ export { sliceFieldAdmin };
@@ -0,0 +1,8 @@
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
+ export {};
@@ -19,6 +19,7 @@ const placeholderForField = (f)=>{
19
19
  switch(f.type){
20
20
  case 'text':
21
21
  case 'textArea':
22
+ case 'code':
22
23
  return '';
23
24
  case 'checkbox':
24
25
  return false;
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- import type { Field, FieldComponentSlots, RichTextEditorComponent } from '@byline/core';
8
+ import type { Field, FieldAdminConfig, FieldComponentSlots, RichTextEditorComponent } from '@byline/core';
9
9
  interface FieldRendererProps {
10
10
  field: Field;
11
11
  defaultValue?: any;
@@ -32,6 +32,15 @@ interface FieldRendererProps {
32
32
  * Ignored when `field.type !== 'richText'`.
33
33
  */
34
34
  editor?: RichTextEditorComponent;
35
+ /**
36
+ * Admin overrides for this field's *descendants*, keyed by dotted,
37
+ * index-free schema paths relative to this field ('answer',
38
+ * 'filesGroup.publicationFile'). Only meaningful when `field` is a
39
+ * structural `group` / `array` — the widget slices the map per child
40
+ * (see `sliceFieldAdmin`). `components` / `editor` above stay the
41
+ * overrides for this field itself.
42
+ */
43
+ fieldAdmin?: Record<string, FieldAdminConfig>;
35
44
  }
36
- export declare const FieldRenderer: ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor, }: FieldRendererProps) => import("react").JSX.Element | null;
45
+ export declare const FieldRenderer: ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor, fieldAdmin, }: FieldRendererProps) => import("react").JSX.Element | null;
37
46
  export {};
@@ -5,6 +5,7 @@ import { useFormContext } from "../forms/form-context.js";
5
5
  import { ArrayField } from "./array/array-field.js";
6
6
  import { BlocksField } from "./blocks/blocks-field.js";
7
7
  import { CheckboxField } from "./checkbox/checkbox-field.js";
8
+ import { CodeField } from "./code/code-field.js";
8
9
  import { DateTimeField } from "./datetime/datetime-field.js";
9
10
  import field_renderer_module from "./field-renderer.module.js";
10
11
  import { FileField } from "./file/file-field.js";
@@ -19,7 +20,7 @@ import { TextField } from "./text/text-field.js";
19
20
  import { TextAreaField } from "./text-area/text-area-field.js";
20
21
  import { useFieldChangeHandler } from "./use-field-change-handler.js";
21
22
  import { useFieldCondition } from "./use-field-condition.js";
22
- const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor })=>{
23
+ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, collectionPath, contentLocale, components, editor, fieldAdmin })=>{
23
24
  const path = basePath ? `${basePath}.${field.name}` : field.name;
24
25
  const htmlId = path.replace(/[[\].]/g, '-');
25
26
  const handleChange = useFieldChangeHandler(field, path);
@@ -60,6 +61,19 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
60
61
  locale: isLocalised ? contentLocale : void 0,
61
62
  components: components
62
63
  });
64
+ case 'code':
65
+ return /*#__PURE__*/ jsx(CodeField, {
66
+ field: hideLabel ? {
67
+ ...field,
68
+ label: void 0
69
+ } : field,
70
+ defaultValue: defaultValue,
71
+ onChange: handleChange,
72
+ path: path,
73
+ id: htmlId,
74
+ locale: isLocalised ? contentLocale : void 0,
75
+ components: components
76
+ });
63
77
  case 'checkbox':
64
78
  return /*#__PURE__*/ jsx(CheckboxField, {
65
79
  field: hideLabel ? {
@@ -189,8 +203,10 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
189
203
  } : field,
190
204
  defaultValue: defaultValue,
191
205
  path: path,
206
+ disableSorting: disableSorting,
192
207
  collectionPath: collectionPath,
193
- contentLocale: contentLocale
208
+ contentLocale: contentLocale,
209
+ fieldAdmin: fieldAdmin
194
210
  });
195
211
  case 'blocks':
196
212
  if (!field.blocks) return null;
@@ -208,13 +224,14 @@ const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableS
208
224
  path: path,
209
225
  disableSorting: disableSorting,
210
226
  collectionPath: collectionPath,
211
- contentLocale: contentLocale
227
+ contentLocale: contentLocale,
228
+ fieldAdmin: fieldAdmin
212
229
  });
213
230
  default:
214
231
  return null;
215
232
  }
216
233
  };
217
- const selfBadge = 'text' === field.type || 'textArea' === field.type || 'richText' === field.type;
234
+ const selfBadge = 'text' === field.type || 'textArea' === field.type || 'code' === field.type || 'richText' === field.type;
218
235
  if (badge && !selfBadge) return /*#__PURE__*/ jsxs("div", {
219
236
  className: classnames('byline-field-localized-wrap', field_renderer_module["localized-wrap"]),
220
237
  children: [
@@ -5,11 +5,21 @@
5
5
  *
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
- import type { GroupField as GroupFieldType } from '@byline/core';
8
+ import type { FieldAdminConfig, GroupField as GroupFieldType } from '@byline/core';
9
9
  interface GroupFieldProps {
10
10
  field: GroupFieldType;
11
11
  defaultValue: any;
12
12
  path: string;
13
+ /**
14
+ * Threaded to child fields — governs only the *drag* affordance of any
15
+ * `array` children (structural add/remove always renders; see ArrayField).
16
+ * Defaults to `true` (conservative): arrays inside plain schema groups
17
+ * stay drag-free. `BlocksField` passes `false` on its synthesized group so
18
+ * arrays directly inside blocks are fully sortable — safe because each
19
+ * `DraggableSortable` is an independent DndContext with grip-scoped
20
+ * listeners.
21
+ */
22
+ disableSorting?: boolean;
13
23
  /**
14
24
  * Collection path forwarded to upload-capable child fields (`file` / `image`),
15
25
  * which need it to reach the `/upload` endpoint. Without it those fields fall
@@ -22,6 +32,17 @@ interface GroupFieldProps {
22
32
  * locale badge.
23
33
  */
24
34
  contentLocale?: string;
35
+ /**
36
+ * Per-child-field admin overrides (`components` slots, richtext `editor`),
37
+ * keyed by dotted, index-free schema paths relative to this group
38
+ * ('caption', 'faq.answer'). Threaded by `BlocksField` from the site-wide
39
+ * `ClientConfig.blockAdmin` registry (block children render through a
40
+ * synthesized group) and by `FieldRenderer` for plain schema groups, whose
41
+ * map arrives pre-sliced from the collection admin config. Exact-name
42
+ * entries apply to the child itself; deeper entries are re-sliced and
43
+ * threaded on (see `sliceFieldAdmin`).
44
+ */
45
+ fieldAdmin?: Record<string, FieldAdminConfig>;
25
46
  }
26
- export declare const GroupField: ({ field, defaultValue, path, collectionPath, contentLocale, }: GroupFieldProps) => import("react").JSX.Element;
47
+ export declare const GroupField: ({ field, defaultValue, path, disableSorting, collectionPath, contentLocale, fieldAdmin, }: GroupFieldProps) => import("react").JSX.Element;
27
48
  export {};
@@ -2,11 +2,12 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from "react";
3
3
  import { ErrorText } from "@byline/ui/react";
4
4
  import classnames from "classnames";
5
+ import { sliceFieldAdmin } from "../field-admin.js";
5
6
  import { placeholderForField } from "../field-helpers.js";
6
7
  import { FieldRenderer } from "../field-renderer.js";
7
8
  import { useFieldError } from "../../forms/form-context.js";
8
9
  import group_field_module from "./group-field.module.js";
9
- const GroupField = ({ field, defaultValue, path, collectionPath, contentLocale })=>{
10
+ const GroupField = ({ field, defaultValue, path, disableSorting = true, collectionPath, contentLocale, fieldAdmin })=>{
10
11
  const fieldError = useFieldError(field.name);
11
12
  const groupData = useMemo(()=>{
12
13
  if (defaultValue && 'object' == typeof defaultValue && !Array.isArray(defaultValue)) return defaultValue;
@@ -46,9 +47,12 @@ const GroupField = ({ field, defaultValue, path, collectionPath, contentLocale }
46
47
  field: innerField,
47
48
  defaultValue: groupData[innerField.name],
48
49
  basePath: path,
49
- disableSorting: true,
50
+ disableSorting: disableSorting,
50
51
  collectionPath: collectionPath,
51
- contentLocale: contentLocale
52
+ contentLocale: contentLocale,
53
+ components: fieldAdmin?.[innerField.name]?.components,
54
+ editor: fieldAdmin?.[innerField.name]?.editor,
55
+ fieldAdmin: sliceFieldAdmin(fieldAdmin, innerField.name)
52
56
  }, innerField.name))
53
57
  }),
54
58
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
@@ -39,13 +39,18 @@ const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField,
39
39
  const selectFields = resolveSelectFields(targetDefinition, displayField, pickerColumns, extraSelectFields);
40
40
  setLoading(true);
41
41
  setError(null);
42
+ const itemViewSort = targetAdminConfig?.itemViewSort;
42
43
  getCollectionDocuments({
43
44
  collection: targetCollectionPath,
44
45
  params: {
45
46
  page,
46
47
  page_size: PAGE_SIZE,
47
48
  query: query.length > 0 ? query : void 0,
48
- fields: selectFields
49
+ fields: selectFields,
50
+ ...null != itemViewSort ? {
51
+ order: String(itemViewSort.field),
52
+ desc: 'desc' === itemViewSort.direction
53
+ } : {}
49
54
  }
50
55
  }).then((response)=>{
51
56
  if (cancelled) return;
@@ -71,7 +76,8 @@ const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField,
71
76
  targetDefinition,
72
77
  pickerColumns,
73
78
  getCollectionDocuments,
74
- t
79
+ t,
80
+ targetAdminConfig?.itemViewSort
75
81
  ]);
76
82
  const resolvedDisplayField = displayField ?? targetDefinition?.useAsTitle ?? resolveFallbackDisplayField(targetDefinition) ?? null;
77
83
  const handleSelect = useCallback(()=>{
@@ -17,10 +17,12 @@ const SelectField = ({ field, value, defaultValue, onChange, id, path })=>{
17
17
  id: htmlId,
18
18
  htmlFor: htmlId,
19
19
  label: field.label,
20
- required: !field.optional
20
+ required: !field.optional,
21
+ className: classnames('byline-field-select-label', select_field_module.label)
21
22
  }),
22
23
  /*#__PURE__*/ jsx(Select, {
23
- size: "sm",
24
+ size: "xs",
25
+ variant: "outlined",
24
26
  id: htmlId,
25
27
  name: field.name,
26
28
  placeholder: "Select an option",
@@ -1,5 +1,6 @@
1
1
  import "./select-field_module.css";
2
2
  const select_field_module = {
3
+ label: "label-ScZ_3S",
3
4
  dirty: "dirty-aKaGZN"
4
5
  };
5
6
  export default select_field_module;
@@ -1,3 +1,7 @@
1
+ :is(.label-ScZ_3S, .byline-field-select-label) {
2
+ margin-bottom: .25rem;
3
+ }
4
+
1
5
  :is(.dirty-aKaGZN, .byline-field-select-dirty) {
2
6
  border-color: var(--blue-300);
3
7
  }
@@ -13,3 +13,9 @@ export declare const SortableItem: ({ id, label, children, onAddBelow, onRemove,
13
13
  onAddBelow?: () => void;
14
14
  onRemove?: () => void;
15
15
  }) => import("react").JSX.Element;
16
+ export declare const StaticItem: ({ label, children, onAddBelow, onRemove, }: {
17
+ label: ReactNode;
18
+ children: ReactNode;
19
+ onAddBelow?: () => void;
20
+ onRemove?: () => void;
21
+ }) => import("react").JSX.Element;
@@ -5,25 +5,13 @@ import { ChevronDownIcon, GripperVerticalIcon, useSortable } from "@byline/ui/re
5
5
  import classnames from "classnames";
6
6
  import { DraggableContextMenu } from "./draggable-context-menu.js";
7
7
  import sortable_item_module from "./sortable-item.module.js";
8
- const SortableItem = ({ id, label, children, onAddBelow, onRemove })=>{
8
+ const ItemFrame = ({ label, children, onAddBelow, onRemove, grip, rootRef, style, dragging = false, rootClassName })=>{
9
9
  const { t } = useTranslation('byline-admin');
10
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
11
- id,
12
- transition: {
13
- duration: 250,
14
- easing: 'cubic-bezier(0, 0.2, 0.2, 1)'
15
- }
16
- });
17
10
  const [collapsed, setCollapsed] = useState(false);
18
- const style = {
19
- transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : void 0,
20
- transition,
21
- zIndex: isDragging ? 10 : 'auto'
22
- };
23
11
  return /*#__PURE__*/ jsxs("div", {
24
- ref: setNodeRef,
12
+ ref: rootRef,
25
13
  style: style,
26
- className: classnames('byline-sortable', sortable_item_module.root, isDragging && [
14
+ className: classnames('byline-sortable', sortable_item_module.root, rootClassName, dragging && [
27
15
  'byline-sortable-dragging',
28
16
  sortable_item_module.dragging
29
17
  ], collapsed && [
@@ -37,15 +25,7 @@ const SortableItem = ({ id, label, children, onAddBelow, onRemove })=>{
37
25
  sortable_item_module["header-expanded"]
38
26
  ]),
39
27
  children: [
40
- /*#__PURE__*/ jsx("button", {
41
- type: "button",
42
- className: classnames('byline-sortable-grip', sortable_item_module.grip),
43
- ...attributes,
44
- ...listeners,
45
- children: /*#__PURE__*/ jsx(GripperVerticalIcon, {
46
- className: classnames('byline-sortable-grip-icon', sortable_item_module["grip-icon"])
47
- })
48
- }),
28
+ grip,
49
29
  /*#__PURE__*/ jsx("div", {
50
30
  className: classnames('byline-sortable-label', sortable_item_module.label),
51
31
  children: label
@@ -78,4 +58,43 @@ const SortableItem = ({ id, label, children, onAddBelow, onRemove })=>{
78
58
  ]
79
59
  });
80
60
  };
81
- export { SortableItem };
61
+ const SortableItem = ({ id, label, children, onAddBelow, onRemove })=>{
62
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
63
+ id,
64
+ transition: {
65
+ duration: 250,
66
+ easing: 'cubic-bezier(0, 0.2, 0.2, 1)'
67
+ }
68
+ });
69
+ const style = {
70
+ transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : void 0,
71
+ transition,
72
+ zIndex: isDragging ? 10 : 'auto'
73
+ };
74
+ return /*#__PURE__*/ jsx(ItemFrame, {
75
+ label: label,
76
+ onAddBelow: onAddBelow,
77
+ onRemove: onRemove,
78
+ rootRef: setNodeRef,
79
+ style: style,
80
+ dragging: isDragging,
81
+ grip: /*#__PURE__*/ jsx("button", {
82
+ type: "button",
83
+ className: classnames('byline-sortable-grip', sortable_item_module.grip),
84
+ ...attributes,
85
+ ...listeners,
86
+ children: /*#__PURE__*/ jsx(GripperVerticalIcon, {
87
+ className: classnames('byline-sortable-grip-icon', sortable_item_module["grip-icon"])
88
+ })
89
+ }),
90
+ children: children
91
+ });
92
+ };
93
+ const StaticItem = ({ label, children, onAddBelow, onRemove })=>/*#__PURE__*/ jsx(ItemFrame, {
94
+ label: label,
95
+ onAddBelow: onAddBelow,
96
+ onRemove: onRemove,
97
+ rootClassName: "byline-sortable-static",
98
+ children: children
99
+ });
100
+ export { SortableItem, StaticItem };
@@ -1,5 +1,6 @@
1
1
  :is(.label-row-sjBuRb, .byline-field-text-label-row) {
2
2
  align-items: center;
3
+ margin-bottom: .25rem;
3
4
  display: flex;
4
5
  }
5
6
 
@@ -1,5 +1,6 @@
1
1
  :is(.label-row-HwX7C9, .byline-field-text-area-label-row) {
2
2
  align-items: center;
3
+ margin-bottom: .25rem;
3
4
  display: flex;
4
5
  }
5
6
 
@@ -140,7 +140,7 @@ const FormProvider = ({ children, initialData = {}, documentId = null })=>{
140
140
  const appendPatch = useCallback((patch)=>{
141
141
  patchesRef.current = [
142
142
  ...patchesRef.current,
143
- patch
143
+ structuredClone(patch)
144
144
  ];
145
145
  dirtyFields.current.add('__patch__');
146
146
  notifyMetaListeners();
@@ -104,7 +104,7 @@ export interface FormRendererProps {
104
104
  * Opts the document-tree placement widget into the sidebar (above the
105
105
  * available-locales widget). Sourced from `CollectionDefinition.tree` by the
106
106
  * caller. Renders only in edit mode (placement needs a persisted document)
107
- * and only when the host wires the tree services. See docs/04-collections/03-document-trees.md.
107
+ * and only when the host wires the tree services. See docs/04-collections/04-document-trees.md.
108
108
  */
109
109
  tree?: boolean;
110
110
  headingLabel?: string;
@@ -5,6 +5,7 @@ import { getClientConfig } from "@byline/core";
5
5
  import { useTranslation } from "@byline/i18n/react";
6
6
  import { Alert, Button, ComboButton } from "@byline/ui/react";
7
7
  import classnames from "classnames";
8
+ import { sliceFieldAdmin } from "../fields/field-admin.js";
8
9
  import { FieldRenderer } from "../fields/field-renderer.js";
9
10
  import { useBylineFieldServices } from "../fields/field-services-context.js";
10
11
  import { AdminGroup } from "../presentation/group.js";
@@ -185,7 +186,8 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
185
186
  collectionPath: collectionPath,
186
187
  contentLocale: contentLocale,
187
188
  components: adminConfig?.fields?.[field.name]?.components,
188
- editor: adminConfig?.fields?.[field.name]?.editor
189
+ editor: adminConfig?.fields?.[field.name]?.editor,
190
+ fieldAdmin: sliceFieldAdmin(adminConfig?.fields, field.name)
189
191
  }, field.name);
190
192
  };
191
193
  const renderItem = (name)=>{
@@ -8,7 +8,7 @@ export interface TreePlacementWidgetProps {
8
8
  }
9
9
  /**
10
10
  * Sidebar widget for placing the current document within its collection's
11
- * single-parent document tree (the `tree: true` primitive — docs/04-collections/03-document-trees.md).
11
+ * single-parent document tree (the `tree: true` primitive — docs/04-collections/04-document-trees.md).
12
12
  *
13
13
  * The tree is document-grain and **unversioned**, so changes here write
14
14
  * immediately (independent of the form's content save). The editor picks a