@byline/admin 6.2.1 → 6.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 (55) hide show
  1. package/dist/fields/checkbox/checkbox-field.js +3 -1
  2. package/dist/fields/code/code-field.js +2 -1
  3. package/dist/fields/datetime/datetime-field.js +4 -2
  4. package/dist/fields/field-renderer.js +2 -1
  5. package/dist/fields/field-services-types.d.ts +23 -0
  6. package/dist/fields/file/file-field.js +3 -2
  7. package/dist/fields/group/group-field.js +3 -1
  8. package/dist/fields/image/image-field.js +3 -2
  9. package/dist/fields/numerical/numerical-field.js +2 -1
  10. package/dist/fields/relation/relation-field.js +3 -2
  11. package/dist/fields/relation/relation-many-field.js +3 -2
  12. package/dist/fields/relation/relation-picker.js +38 -7
  13. package/dist/fields/relation/relation-picker.module.js +3 -0
  14. package/dist/fields/relation/relation-picker_module.css +20 -0
  15. package/dist/fields/select/select-field.js +3 -2
  16. package/dist/fields/text/text-field.js +2 -1
  17. package/dist/fields/text-area/text-area-field.js +2 -1
  18. package/dist/forms/form-context.js +4 -1
  19. package/dist/forms/form-dom-scope.d.ts +34 -0
  20. package/dist/forms/form-dom-scope.js +20 -0
  21. package/dist/forms/form-layout.js +3 -1
  22. package/dist/forms/form-renderer.js +1 -0
  23. package/dist/presentation/tabs.js +2 -1
  24. package/dist/react.d.ts +1 -1
  25. package/package.json +7 -7
  26. package/src/fields/checkbox/checkbox-field.tsx +3 -1
  27. package/src/fields/code/code-field.tsx +2 -1
  28. package/src/fields/datetime/datetime-field.tsx +4 -2
  29. package/src/fields/direct-widget-dom-ids.test.tsx +142 -0
  30. package/src/fields/field-dom-ids.test.tsx +293 -0
  31. package/src/fields/field-renderer.tsx +2 -1
  32. package/src/fields/field-services-types.ts +25 -0
  33. package/src/fields/file/file-field.tsx +3 -2
  34. package/src/fields/group/group-field.tsx +3 -1
  35. package/src/fields/image/image-field.tsx +3 -2
  36. package/src/fields/numerical/numerical-field.tsx +2 -1
  37. package/src/fields/relation/relation-field.tsx +3 -2
  38. package/src/fields/relation/relation-many-field.tsx +3 -2
  39. package/src/fields/relation/relation-picker-create.test.tsx +203 -0
  40. package/src/fields/relation/relation-picker-refresh.test.tsx +325 -0
  41. package/src/fields/relation/relation-picker.module.css +32 -0
  42. package/src/fields/relation/relation-picker.tsx +59 -1
  43. package/src/fields/select/select-field.tsx +3 -2
  44. package/src/fields/text/text-field.tsx +2 -1
  45. package/src/fields/text-area/text-area-field.tsx +2 -1
  46. package/src/forms/form-context.tsx +2 -1
  47. package/src/forms/form-dom-scope.test.tsx +213 -0
  48. package/src/forms/form-dom-scope.tsx +70 -0
  49. package/src/forms/form-layout.tsx +7 -1
  50. package/src/forms/form-renderer.tsx +11 -0
  51. package/src/forms/form-submit-isolation.test.tsx +247 -0
  52. package/src/presentation/tab-dom-ids.test.tsx +153 -0
  53. package/src/presentation/tabs.test.tsx +49 -2
  54. package/src/presentation/tabs.tsx +11 -1
  55. package/src/react.ts +2 -0
@@ -1,16 +1,18 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { Checkbox } from "@byline/ui/react";
3
3
  import { useFieldError, useFieldValue } from "../../forms/form-context.js";
4
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
4
5
  import checkbox_field_module from "./checkbox-field.module.js";
5
6
  const CheckboxField = ({ field, value, defaultValue, onChange, id, path })=>{
6
7
  const fieldPath = path ?? field.name;
8
+ const htmlId = useScopedDomId(fieldPath, id);
7
9
  const fieldError = useFieldError(fieldPath);
8
10
  const fieldValue = useFieldValue(fieldPath);
9
11
  const checked = value ?? fieldValue ?? defaultValue ?? false;
10
12
  return /*#__PURE__*/ jsx("div", {
11
13
  className: `byline-field-checkbox ${field.name} ${checkbox_field_module["field-checkbox"]}`,
12
14
  children: /*#__PURE__*/ jsx(Checkbox, {
13
- id: id ?? fieldPath,
15
+ id: htmlId,
14
16
  name: field.name,
15
17
  label: field.label,
16
18
  checked: checked,
@@ -3,6 +3,7 @@ import react, { Suspense, useCallback } from "react";
3
3
  import { ErrorText, HelpText, Label } from "@byline/ui/react";
4
4
  import clsx from "clsx";
5
5
  import { useFieldError, useFieldValue } from "../../forms/form-context.js";
6
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
6
7
  import { LocaleBadge } from "../locale-badge.js";
7
8
  import code_field_module from "./code-field.module.js";
8
9
  const CodeEditor = /*#__PURE__*/ react.lazy(()=>import("./code-editor.js"));
@@ -15,7 +16,7 @@ const CodeField = ({ field, value, defaultValue, onChange, id, path, locale, com
15
16
  const fieldError = useFieldError(fieldPath);
16
17
  const fieldValue = useFieldValue(fieldPath);
17
18
  const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
18
- const htmlId = id ?? fieldPath;
19
+ const htmlId = useScopedDomId(fieldPath, id);
19
20
  const siblingPath = field.languageField ? siblingFieldPath(fieldPath, field.languageField) : fieldPath;
20
21
  const siblingLanguage = useFieldValue(siblingPath);
21
22
  const effectiveLanguage = (field.languageField ? siblingLanguage : void 0) || field.language;
@@ -2,9 +2,11 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { DatePicker, ErrorText } from "@byline/ui/react";
3
3
  import clsx from "clsx";
4
4
  import { useFieldError, useFieldValue, useIsDirty } from "../../forms/form-context.js";
5
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
5
6
  import datetime_field_module from "./datetime-field.module.js";
6
7
  const DateTimeField = ({ field, value, defaultValue, onChange, id, path })=>{
7
8
  const fieldPath = path ?? field.name;
9
+ const htmlId = useScopedDomId(fieldPath, id);
8
10
  const fieldError = useFieldError(fieldPath);
9
11
  const isDirty = useIsDirty(fieldPath);
10
12
  const fieldValue = useFieldValue(fieldPath);
@@ -13,7 +15,7 @@ const DateTimeField = ({ field, value, defaultValue, onChange, id, path })=>{
13
15
  className: `byline-field-datetime ${field.name}`,
14
16
  children: [
15
17
  /*#__PURE__*/ jsx(DatePicker, {
16
- id: id ?? fieldPath,
18
+ id: htmlId,
17
19
  name: field.name,
18
20
  label: field.label,
19
21
  required: !field.optional,
@@ -28,7 +30,7 @@ const DateTimeField = ({ field, value, defaultValue, onChange, id, path })=>{
28
30
  ])
29
31
  }),
30
32
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
31
- id: `${field.name}-error`,
33
+ id: `${htmlId}-error`,
32
34
  text: fieldError
33
35
  })
34
36
  ]
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { getAdminConfig } from "@byline/core";
3
3
  import clsx from "clsx";
4
4
  import { useFormContext } from "../forms/form-context.js";
5
+ import { useScopedDomId } from "../forms/form-dom-scope.js";
5
6
  import { ArrayField } from "./array/array-field.js";
6
7
  import { BlocksField } from "./blocks/blocks-field.js";
7
8
  import { CheckboxField } from "./checkbox/checkbox-field.js";
@@ -22,7 +23,7 @@ import { useFieldChangeHandler } from "./use-field-change-handler.js";
22
23
  import { useFieldCondition } from "./use-field-condition.js";
23
24
  const FieldRenderer = ({ field, defaultValue: initialDefault, basePath, disableSorting, hideLabel, contentLocale, components, editor, fieldAdmin })=>{
24
25
  const path = basePath ? `${basePath}.${field.name}` : field.name;
25
- const htmlId = path.replace(/[[\].]/g, '-');
26
+ const htmlId = useScopedDomId(path.replace(/[[\].]/g, '-'));
26
27
  const handleChange = useFieldChangeHandler(field, path);
27
28
  const { getFieldValue } = useFormContext();
28
29
  const visible = useFieldCondition(field, basePath);
@@ -101,6 +101,10 @@ export type GetTreeParentFn = (input: {
101
101
  placed: boolean;
102
102
  parentDocumentId: string | null;
103
103
  }>;
104
+ /** See `BylineFieldServices.canCreateInCollection`. */
105
+ export type CanCreateInCollectionFn = (collectionPath: string) => boolean;
106
+ /** See `BylineFieldServices.getCreateDocumentUrl`. */
107
+ export type GetCreateDocumentUrlFn = (collectionPath: string) => string;
104
108
  export interface BylineFieldServices {
105
109
  getCollectionDocuments: GetCollectionDocumentsFn;
106
110
  uploadField: UploadFieldFn;
@@ -113,4 +117,23 @@ export interface BylineFieldServices {
113
117
  removeFromTree?: RemoveFromTreeFn;
114
118
  getTreeAncestors?: GetTreeAncestorsFn;
115
119
  getTreeParent?: GetTreeParentFn;
120
+ /**
121
+ * Whether the viewer may create documents in a collection, used to decide
122
+ * whether to offer a create affordance in the relation picker.
123
+ *
124
+ * Cosmetic only: the create view enforces the ability server-side regardless
125
+ * of what this returns. Optional, like the tree operations above — a host that
126
+ * wires neither this nor `getCreateDocumentUrl` simply gets no affordance, and
127
+ * the widget guards on their presence.
128
+ */
129
+ canCreateInCollection?: CanCreateInCollectionFn;
130
+ /**
131
+ * Root-relative URL of a collection's create view, built from the host's
132
+ * configured admin path.
133
+ *
134
+ * Root-relative rather than absolute because no origin is available during the
135
+ * server render, and the admin base path is host configuration —
136
+ * `@byline/admin` must not assume `/admin`.
137
+ */
138
+ getCreateDocumentUrl?: GetCreateDocumentUrlFn;
116
139
  }
@@ -4,6 +4,7 @@ import { useTranslation } from "@byline/i18n/react";
4
4
  import { CloseIcon, DocumentIcon, DownloadIcon, ErrorText, HelpText, IconButton, Label, LoaderRing, VideoIcon } from "@byline/ui/react";
5
5
  import clsx from "clsx";
6
6
  import { useFieldError, useFieldValue, useFormContext, useIsDirty, useIsFieldUploading } from "../../forms/form-context.js";
7
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
7
8
  import { useFieldChangeHandler } from "../use-field-change-handler.js";
8
9
  import file_field_module from "./file-field.module.js";
9
10
  import { FileUploadField } from "./file-upload-field.js";
@@ -42,7 +43,7 @@ const FileField = ({ field, value, defaultValue, onChange: _onChange, path })=>{
42
43
  };
43
44
  const isVideo = incomingValue?.mimeType?.startsWith('video/') === true;
44
45
  const FileGlyph = isVideo ? VideoIcon : DocumentIcon;
45
- const htmlId = fieldPath;
46
+ const htmlId = useScopedDomId(fieldPath);
46
47
  return /*#__PURE__*/ jsxs("div", {
47
48
  className: `byline-field-file ${field.name}`,
48
49
  children: [
@@ -222,7 +223,7 @@ const FileField = ({ field, value, defaultValue, onChange: _onChange, path })=>{
222
223
  text: field.helpText
223
224
  }),
224
225
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
225
- id: `${field.name}-error`,
226
+ id: `${htmlId}-error`,
226
227
  text: fieldError
227
228
  })
228
229
  ]
@@ -6,8 +6,10 @@ import { sliceFieldAdmin } from "../field-admin.js";
6
6
  import { placeholderForField } from "../field-helpers.js";
7
7
  import { FieldRenderer } from "../field-renderer.js";
8
8
  import { useFieldError } from "../../forms/form-context.js";
9
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
9
10
  import group_field_module from "./group-field.module.js";
10
11
  const GroupField = ({ field, defaultValue, path, disableSorting = true, contentLocale, fieldAdmin })=>{
12
+ const htmlId = useScopedDomId(path ?? field.name);
11
13
  const fieldError = useFieldError(field.name);
12
14
  const groupData = useMemo(()=>{
13
15
  if (defaultValue && 'object' == typeof defaultValue && !Array.isArray(defaultValue)) return defaultValue;
@@ -55,7 +57,7 @@ const GroupField = ({ field, defaultValue, path, disableSorting = true, contentL
55
57
  }, innerField.name))
56
58
  }),
57
59
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
58
- id: `${field.name}-error`,
60
+ id: `${htmlId}-error`,
59
61
  text: fieldError
60
62
  })
61
63
  ]
@@ -5,6 +5,7 @@ import { useTranslation } from "@byline/i18n/react";
5
5
  import { CloseIcon, ErrorText, HelpText, IconButton, ImageLightbox, Label, LoaderRing } from "@byline/ui/react";
6
6
  import clsx from "clsx";
7
7
  import { useFieldError, useFieldValue, useFormContext, useIsDirty, useIsFieldUploading } from "../../forms/form-context.js";
8
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
8
9
  import { useFieldChangeHandler } from "../use-field-change-handler.js";
9
10
  import image_field_module from "./image-field.module.js";
10
11
  import { ImageUploadField } from "./image-upload-field.js";
@@ -34,7 +35,7 @@ const ImageField = ({ field, value, defaultValue, onChange: _onChange, path })=>
34
35
  };
35
36
  const [lightboxOpen, setLightboxOpen] = useState(false);
36
37
  const canOpenLightbox = !isPending && !!incomingValue?.storageUrl;
37
- const htmlId = fieldPath;
38
+ const htmlId = useScopedDomId(fieldPath);
38
39
  return /*#__PURE__*/ jsxs("div", {
39
40
  className: `byline-field-image ${field.name}`,
40
41
  children: [
@@ -224,7 +225,7 @@ const ImageField = ({ field, value, defaultValue, onChange: _onChange, path })=>
224
225
  text: field.helpText
225
226
  }),
226
227
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
227
- id: `${field.name}-error`,
228
+ id: `${htmlId}-error`,
228
229
  text: fieldError
229
230
  }),
230
231
  canOpenLightbox && incomingValue?.storageUrl && /*#__PURE__*/ jsx(ImageLightbox, {
@@ -3,13 +3,14 @@ import { useEffect, useState } from "react";
3
3
  import { normalizeNumericValue } from "@byline/core";
4
4
  import { Input } from "@byline/ui/react";
5
5
  import { useFieldError, useFieldValue } from "../../forms/form-context.js";
6
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
6
7
  const COMPLETE_NUMERIC_RE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
7
8
  const NumericalField = ({ field, value, defaultValue, onChange, id, path, components })=>{
8
9
  const fieldPath = path ?? field.name;
9
10
  const fieldError = useFieldError(fieldPath);
10
11
  const fieldValue = useFieldValue(fieldPath);
11
12
  const incomingValue = void 0 !== value ? value : void 0 !== fieldValue ? fieldValue : defaultValue ?? null;
12
- const htmlId = id ?? fieldPath;
13
+ const htmlId = useScopedDomId(fieldPath, id);
13
14
  const canonicalDisplay = null == incomingValue ? '' : String(incomingValue);
14
15
  const [displayValue, setDisplayValue] = useState(canonicalDisplay);
15
16
  const [isEditing, setIsEditing] = useState(false);
@@ -5,12 +5,13 @@ import { useTranslation } from "@byline/i18n/react";
5
5
  import { Button, CloseIcon, EditIcon, ErrorText, HelpText, IconButton, Label } from "@byline/ui/react";
6
6
  import clsx from "clsx";
7
7
  import { useFieldError, useFieldValue } from "../../forms/form-context.js";
8
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
8
9
  import relation_field_module from "./relation-field.module.js";
9
10
  import { RelationPicker } from "./relation-picker.js";
10
11
  import { RelationSummary } from "./relation-summary.js";
11
12
  const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
12
13
  const fieldPath = path ?? field.name;
13
- const htmlId = id ?? fieldPath;
14
+ const htmlId = useScopedDomId(fieldPath, id);
14
15
  const fieldError = useFieldError(fieldPath);
15
16
  const fieldValue = useFieldValue(fieldPath);
16
17
  const incomingValue = void 0 !== fieldValue ? fieldValue ?? null : value ?? defaultValue ?? null;
@@ -124,7 +125,7 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
124
125
  text: field.helpText
125
126
  }),
126
127
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
127
- id: `${field.name}-error`,
128
+ id: `${htmlId}-error`,
128
129
  text: fieldError
129
130
  }),
130
131
  !isUnknown && /*#__PURE__*/ jsx(RelationPicker, {
@@ -5,6 +5,7 @@ import { useTranslation } from "@byline/i18n/react";
5
5
  import { Button, CloseIcon, DraggableSortable, ErrorText, GripperVerticalIcon, HelpText, IconButton, Label, PlusIcon, moveItem, useSortable } from "@byline/ui/react";
6
6
  import clsx from "clsx";
7
7
  import { useFieldError, useFieldValue, useFormContext } from "../../forms/form-context.js";
8
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
8
9
  import relation_field_module from "./relation-field.module.js";
9
10
  import { RelationPicker } from "./relation-picker.js";
10
11
  import { RelationSummary } from "./relation-summary.js";
@@ -57,7 +58,7 @@ const RelationManyTile = ({ id, children, onRemove, removeAriaLabel })=>{
57
58
  };
58
59
  const RelationManyField = ({ field, defaultValue, id, path })=>{
59
60
  const fieldPath = path ?? field.name;
60
- const htmlId = id ?? fieldPath;
61
+ const htmlId = useScopedDomId(fieldPath, id);
61
62
  const { t } = useTranslation('byline-admin');
62
63
  const fieldError = useFieldError(fieldPath);
63
64
  const { getFieldValue, setFieldValue } = useFormContext();
@@ -185,7 +186,7 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
185
186
  text: field.helpText
186
187
  }),
187
188
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
188
- id: `${field.name}-error`,
189
+ id: `${htmlId}-error`,
189
190
  text: fieldError
190
191
  }),
191
192
  /*#__PURE__*/ jsx(RelationPicker, {
@@ -19,7 +19,9 @@ const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField,
19
19
  const [documents, setDocuments] = useState([]);
20
20
  const [totalPages, setTotalPages] = useState(1);
21
21
  const [collectionId, setCollectionId] = useState(null);
22
- const { getCollectionDocuments } = useBylineFieldServices();
22
+ const [refreshNonce, setRefreshNonce] = useState(0);
23
+ const { getCollectionDocuments, canCreateInCollection, getCreateDocumentUrl } = useBylineFieldServices();
24
+ const createHref = null != getCreateDocumentUrl && canCreateInCollection?.(targetCollectionPath) === true ? getCreateDocumentUrl(targetCollectionPath) : null;
23
25
  const targetAdminConfig = getCollectionAdminConfig(targetCollectionPath);
24
26
  const pickerColumns = targetAdminConfig?.itemView;
25
27
  useEffect(()=>{
@@ -77,7 +79,8 @@ const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField,
77
79
  pickerColumns,
78
80
  getCollectionDocuments,
79
81
  t,
80
- targetAdminConfig?.itemViewSort
82
+ targetAdminConfig?.itemViewSort,
83
+ refreshNonce
81
84
  ]);
82
85
  const resolvedDisplayField = displayField ?? targetDefinition?.useAsTitle ?? resolveFallbackDisplayField(targetDefinition) ?? null;
83
86
  const handleSelect = useCallback(()=>{
@@ -127,12 +130,40 @@ const RelationPicker = ({ targetCollectionPath, targetDefinition, displayField,
127
130
  width: '100%'
128
131
  },
129
132
  children: [
130
- /*#__PURE__*/ jsx(Modal.Header, {
133
+ /*#__PURE__*/ jsxs(Modal.Header, {
131
134
  className: clsx('byline-field-relation-picker-header', relation_picker_module.header),
132
- children: /*#__PURE__*/ jsx("h3", {
133
- className: clsx('byline-field-relation-picker-title', relation_picker_module.title),
134
- children: title
135
- })
135
+ children: [
136
+ /*#__PURE__*/ jsx("h3", {
137
+ className: clsx('byline-field-relation-picker-title', relation_picker_module.title),
138
+ children: title
139
+ }),
140
+ /*#__PURE__*/ jsxs("div", {
141
+ className: clsx('byline-field-relation-picker-actions', relation_picker_module.actions),
142
+ children: [
143
+ /*#__PURE__*/ jsx(Button, {
144
+ type: "button",
145
+ size: "xs",
146
+ variant: "outlined",
147
+ className: clsx('byline-field-relation-picker-refresh', relation_picker_module.refresh),
148
+ onClick: ()=>setRefreshNonce((nonce)=>nonce + 1),
149
+ children: t('fields.relation.picker.refresh')
150
+ }),
151
+ null != createHref && /*#__PURE__*/ jsx(Button, {
152
+ size: "xs",
153
+ variant: "outlined",
154
+ className: clsx('byline-field-relation-picker-create', relation_picker_module.create),
155
+ render: /*#__PURE__*/ jsx("a", {
156
+ href: createHref,
157
+ target: "_blank",
158
+ rel: "noopener noreferrer",
159
+ children: t('fields.relation.picker.createNew', {
160
+ label: targetDefinition?.labels.singular ?? targetCollectionPath
161
+ })
162
+ })
163
+ })
164
+ ]
165
+ })
166
+ ]
136
167
  }),
137
168
  /*#__PURE__*/ jsx(Modal.Content, {
138
169
  children: /*#__PURE__*/ jsxs("div", {
@@ -1,6 +1,9 @@
1
1
  import "./relation-picker_module.css";
2
2
  const relation_picker_module = {
3
3
  header: "header-quQOWP",
4
+ actions: "actions-s42_zl",
5
+ refresh: "refresh-CVQwEV",
6
+ create: "create-Ol3jYM",
4
7
  title: "title-nX1CSv",
5
8
  body: "body-S2EcHm",
6
9
  list: "list-UFzd89",
@@ -1,6 +1,26 @@
1
1
  :is(.header-quQOWP, .byline-field-relation-picker-header) {
2
2
  margin-bottom: var(--spacing-8);
3
+ justify-content: space-between;
4
+ align-items: baseline;
5
+ gap: var(--spacing-8);
6
+ flex-wrap: wrap;
3
7
  padding-top: 1rem;
8
+ display: flex;
9
+ }
10
+
11
+ :is(.actions-s42_zl, .byline-field-relation-picker-actions) {
12
+ align-items: center;
13
+ gap: var(--spacing-8);
14
+ display: flex;
15
+ }
16
+
17
+ :is(.refresh-CVQwEV, .byline-field-relation-picker-refresh) {
18
+ white-space: nowrap;
19
+ }
20
+
21
+ :is(.create-Ol3jYM, .byline-field-relation-picker-create) {
22
+ white-space: nowrap;
23
+ text-decoration: none;
4
24
  }
5
25
 
6
26
  :is(.title-nX1CSv, .byline-field-relation-picker-title) {
@@ -2,6 +2,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { ErrorText, Label, Select } from "@byline/ui/react";
3
3
  import clsx from "clsx";
4
4
  import { useFieldError, useFieldValue, useIsDirty } from "../../forms/form-context.js";
5
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
5
6
  import select_field_module from "./select-field.module.js";
6
7
  const SelectField = ({ field, value, defaultValue, onChange, id, path })=>{
7
8
  const fieldPath = path ?? field.name;
@@ -9,7 +10,7 @@ const SelectField = ({ field, value, defaultValue, onChange, id, path })=>{
9
10
  const isDirty = useIsDirty(fieldPath);
10
11
  const fieldValue = useFieldValue(fieldPath);
11
12
  const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
12
- const htmlId = id ?? fieldPath;
13
+ const htmlId = useScopedDomId(fieldPath, id);
13
14
  return /*#__PURE__*/ jsxs("div", {
14
15
  className: `byline-field-select ${field.name}`,
15
16
  children: [
@@ -43,7 +44,7 @@ const SelectField = ({ field, value, defaultValue, onChange, id, path })=>{
43
44
  ])
44
45
  }),
45
46
  fieldError && /*#__PURE__*/ jsx(ErrorText, {
46
- id: `${field.name}-error`,
47
+ id: `${htmlId}-error`,
47
48
  text: fieldError
48
49
  })
49
50
  ]
@@ -3,6 +3,7 @@ import { useCallback } from "react";
3
3
  import { Input, Label } from "@byline/ui/react";
4
4
  import clsx from "clsx";
5
5
  import { useFieldError, useFieldValue } from "../../forms/form-context.js";
6
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
6
7
  import { LocaleBadge } from "../locale-badge.js";
7
8
  import text_field_module from "./text-field.module.js";
8
9
  const TextField = ({ field, value, defaultValue, onChange, id, path, locale, components })=>{
@@ -10,7 +11,7 @@ const TextField = ({ field, value, defaultValue, onChange, id, path, locale, com
10
11
  const fieldError = useFieldError(fieldPath);
11
12
  const fieldValue = useFieldValue(fieldPath);
12
13
  const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
13
- const htmlId = id ?? fieldPath;
14
+ const htmlId = useScopedDomId(fieldPath, id);
14
15
  const handleChange = useCallback((value)=>{
15
16
  if (onChange) onChange(value);
16
17
  }, [
@@ -3,6 +3,7 @@ import { useCallback } from "react";
3
3
  import { Label, TextArea } from "@byline/ui/react";
4
4
  import clsx from "clsx";
5
5
  import { useFieldError, useFieldValue } from "../../forms/form-context.js";
6
+ import { useScopedDomId } from "../../forms/form-dom-scope.js";
6
7
  import { LocaleBadge } from "../locale-badge.js";
7
8
  import text_area_field_module from "./text-area-field.module.js";
8
9
  const TextAreaField = ({ field, value, defaultValue, onChange, id, path, locale, components })=>{
@@ -10,7 +11,7 @@ const TextAreaField = ({ field, value, defaultValue, onChange, id, path, locale,
10
11
  const fieldError = useFieldError(fieldPath);
11
12
  const fieldValue = useFieldValue(fieldPath);
12
13
  const incomingValue = value ?? fieldValue ?? defaultValue ?? '';
13
- const htmlId = id ?? fieldPath;
14
+ const htmlId = useScopedDomId(fieldPath, id);
14
15
  const handleChange = useCallback((value)=>{
15
16
  if (onChange) onChange(value);
16
17
  }, [
@@ -2,6 +2,7 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import { createContext, useCallback, useContext, useEffect, useRef, useState } from "react";
4
4
  import { normalizeHooks } from "@byline/core";
5
+ import { FormDomScopeProvider } from "./form-dom-scope.js";
5
6
  import { get, hasExistingIdTargets, setWithResult } from "./nested-path.js";
6
7
  import { deletePendingUploadsUnderPath } from "./pending-uploads.js";
7
8
  import { useTrackedSlot } from "./use-tracked-slot.js";
@@ -430,7 +431,9 @@ const FormProvider = ({ children, initialData = {}, documentId = null, collectio
430
431
  setSystemAvailableLocales: availableLocalesSlot.set,
431
432
  subscribeSystemAvailableLocales: availableLocalesSlot.subscribe
432
433
  },
433
- children: children
434
+ children: /*#__PURE__*/ jsx(FormDomScopeProvider, {
435
+ children: children
436
+ })
434
437
  });
435
438
  };
436
439
  const useSystemPath = ()=>{
@@ -0,0 +1,34 @@
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 { ReactNode } from 'react';
9
+ /**
10
+ * Establishes one DOM id scope. `FormProvider` wraps its children in this, so a
11
+ * scope exists wherever form state does and no caller has to opt in.
12
+ */
13
+ export declare const FormDomScopeProvider: ({ children }: {
14
+ children: ReactNode;
15
+ }) => ReactNode;
16
+ /**
17
+ * The active scope, or `''` when rendered outside a form. Field widgets are also
18
+ * composed directly by the admin user and role screens, which have no
19
+ * `FormProvider`, so an absent scope degrades to the unprefixed id rather than
20
+ * throwing.
21
+ */
22
+ export declare function useFormDomScope(): string;
23
+ /**
24
+ * Scope a derived DOM id.
25
+ *
26
+ * A caller-supplied `id` is returned untouched: it is the author's identifier, it
27
+ * may already be referenced from outside the form, and scoping it would break that
28
+ * reference as well as double-prefix ids that pass through here twice.
29
+ *
30
+ * The result is a valid HTML id and a valid `for` / `aria-*` reference, but React's
31
+ * generated scope contains characters that need escaping in a CSS selector. Resolve
32
+ * these with `document.getElementById`, not `querySelector('#…')`.
33
+ */
34
+ export declare function useScopedDomId(base: string, explicit?: string): string;
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createContext, useContext, useId } from "react";
4
+ const FormDomScopeContext = /*#__PURE__*/ createContext(null);
5
+ const FormDomScopeProvider = ({ children })=>{
6
+ const scope = useId();
7
+ return /*#__PURE__*/ jsx(FormDomScopeContext.Provider, {
8
+ value: scope,
9
+ children: children
10
+ });
11
+ };
12
+ function useFormDomScope() {
13
+ return useContext(FormDomScopeContext) ?? '';
14
+ }
15
+ function useScopedDomId(base, explicit) {
16
+ const scope = useFormDomScope();
17
+ if (null != explicit) return explicit;
18
+ return '' === scope ? base : `${scope}${base}`;
19
+ }
20
+ export { FormDomScopeProvider, useFormDomScope, useScopedDomId };
@@ -7,6 +7,7 @@ import { FieldRenderer } from "../fields/field-renderer.js";
7
7
  import { AdminGroup } from "../presentation/group.js";
8
8
  import { AdminRow } from "../presentation/row.js";
9
9
  import { AdminTabs, tabPanelId, tabTriggerId } from "../presentation/tabs.js";
10
+ import { useFormDomScope } from "./form-dom-scope.js";
10
11
  import form_renderer_module from "./form-renderer.module.js";
11
12
  import { useFormLayout } from "./use-form-layout.js";
12
13
  import { useFormTabs } from "./use-form-tabs.js";
@@ -17,6 +18,7 @@ const BOTH_REGIONS = [
17
18
  const FormLayout = ({ fields, adminConfig, regions = BOTH_REGIONS, variant = 'split', activeLocale, initialData, activeTabBySet, onTabChange, sidebarSlot })=>{
18
19
  const { fieldByName, tabSetByName, rowByName, groupByName, layout, fieldToTabPath } = useFormLayout(adminConfig, fields);
19
20
  const tabs = useFormTabs(fieldToTabPath, activeTabBySet);
21
+ const domScope = useFormDomScope();
20
22
  const renderField = (fieldName)=>{
21
23
  const field = fieldByName.get(fieldName);
22
24
  if (!field) return null;
@@ -47,7 +49,7 @@ const FormLayout = ({ fields, adminConfig, regions = BOTH_REGIONS, variant = 'sp
47
49
  }, `group:${group.name}`);
48
50
  const renderTabSet = (set)=>{
49
51
  const { visibleTabs, activeTabName, activeTab, errorCounts } = tabs.resolve(set);
50
- const idBase = `tabset:${set.name}`;
52
+ const idBase = `${domScope}tabset:${set.name}`;
51
53
  return /*#__PURE__*/ jsxs("div", {
52
54
  className: clsx('byline-form-tabset', form_renderer_module.tabset),
53
55
  children: [
@@ -149,6 +149,7 @@ const FormContent = ({ mode, mutationIssue, mutationsBlocked = false, observedRe
149
149
  if (onCancel && 'function' == typeof onCancel) onCancel();
150
150
  };
151
151
  const handleSubmit = (e)=>{
152
+ if (e.target !== e.currentTarget) return;
152
153
  e.preventDefault();
153
154
  if (mutationBlockedRef.current) return;
154
155
  submission.submit();
@@ -30,6 +30,7 @@ const AdminTabs = ({ tabs, activeTab, onChange, idBase, errorCounts, className }
30
30
  const tabRefs = useRef(new Map());
31
31
  const focusAfterChange = useRef(null);
32
32
  const [strip, setStrip] = useState(INITIAL_STRIP_STATE);
33
+ const tabsKey = tabs.map((tab)=>`${tab.name}:${tab.label}`).join('\u0000');
33
34
  const measure = useCallback(()=>{
34
35
  const container = containerRef.current;
35
36
  const row = rowRef.current;
@@ -54,7 +55,7 @@ const AdminTabs = ({ tabs, activeTab, onChange, idBase, errorCounts, className }
54
55
  return ()=>observer.disconnect();
55
56
  }, [
56
57
  measure,
57
- tabs
58
+ tabsKey
58
59
  ]);
59
60
  const reveal = useCallback((name)=>{
60
61
  const viewport = viewportRef.current;
package/dist/react.d.ts CHANGED
@@ -67,4 +67,4 @@ export * from './presentation/tabs.js';
67
67
  export * from './widgets/diff-viewer/diff-modal.js';
68
68
  export * from './widgets/source-locale-badge/source-locale-badge.js';
69
69
  export * from './widgets/status-badge/status-badge.js';
70
- export type { BylineFieldServices, CollectionListDoc, CollectionListParams, CollectionListResponse, GetCollectionDocumentsFn, GetTreeAncestorsFn, GetTreeParentFn, PlaceTreeNodeFn, PlaceTreeNodeInput, RemoveFromTreeFn, TreeAncestor, UploadedFileResult, UploadFieldFn, } from './fields/field-services-types.js';
70
+ export type { BylineFieldServices, CanCreateInCollectionFn, CollectionListDoc, CollectionListParams, CollectionListResponse, GetCollectionDocumentsFn, GetCreateDocumentUrlFn, GetTreeAncestorsFn, GetTreeParentFn, PlaceTreeNodeFn, PlaceTreeNodeInput, RemoveFromTreeFn, TreeAncestor, UploadedFileResult, UploadFieldFn, } from './fields/field-services-types.js';
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": "6.2.1",
5
+ "version": "6.3.0",
6
6
  "engines": {
7
7
  "node": ">=20.9.0"
8
8
  },
@@ -183,12 +183,12 @@
183
183
  "react-diff-viewer-continued": "^4.4.0",
184
184
  "uuid": "^14.0.2",
185
185
  "zod": "^4.6.2",
186
- "@byline/analytics": "6.2.1",
187
- "@byline/auth": "6.2.1",
188
- "@byline/ui": "6.2.1",
189
- "@byline/core": "6.2.1",
190
- "@byline/analytics-agent": "6.2.1",
191
- "@byline/i18n": "6.2.1"
186
+ "@byline/analytics": "6.3.0",
187
+ "@byline/core": "6.3.0",
188
+ "@byline/i18n": "6.3.0",
189
+ "@byline/auth": "6.3.0",
190
+ "@byline/ui": "6.3.0",
191
+ "@byline/analytics-agent": "6.3.0"
192
192
  },
193
193
  "peerDependencies": {
194
194
  "react": "^19.0.0",
@@ -10,6 +10,7 @@ import type { CheckboxField as FieldType } from '@byline/core'
10
10
  import { Checkbox } from '@byline/ui/react'
11
11
 
12
12
  import { useFieldError, useFieldValue } from '../../forms/form-context'
13
+ import { useScopedDomId } from '../../forms/form-dom-scope'
13
14
  import styles from './checkbox-field.module.css'
14
15
 
15
16
  export const CheckboxField = ({
@@ -28,6 +29,7 @@ export const CheckboxField = ({
28
29
  path?: string
29
30
  }) => {
30
31
  const fieldPath = path ?? field.name
32
+ const htmlId = useScopedDomId(fieldPath, id)
31
33
  const fieldError = useFieldError(fieldPath)
32
34
  const fieldValue = useFieldValue<boolean | undefined>(fieldPath)
33
35
  const checked = value ?? fieldValue ?? defaultValue ?? false
@@ -35,7 +37,7 @@ export const CheckboxField = ({
35
37
  return (
36
38
  <div className={`byline-field-checkbox ${field.name} ${styles['field-checkbox']}`}>
37
39
  <Checkbox
38
- id={id ?? fieldPath}
40
+ id={htmlId}
39
41
  name={field.name}
40
42
  label={field.label}
41
43
  checked={checked}
@@ -13,6 +13,7 @@ import { ErrorText, HelpText, Label } from '@byline/ui/react'
13
13
  import cx from 'clsx'
14
14
 
15
15
  import { useFieldError, useFieldValue } from '../../forms/form-context'
16
+ import { useScopedDomId } from '../../forms/form-dom-scope'
16
17
  import { LocaleBadge } from '../locale-badge'
17
18
  import styles from './code-field.module.css'
18
19
 
@@ -57,7 +58,7 @@ export const CodeField = ({
57
58
  const fieldError = useFieldError(fieldPath)
58
59
  const fieldValue = useFieldValue<string | undefined>(fieldPath)
59
60
  const incomingValue = value ?? fieldValue ?? defaultValue ?? ''
60
- const htmlId = id ?? fieldPath
61
+ const htmlId = useScopedDomId(fieldPath, id)
61
62
 
62
63
  // Effective highlight language: a sibling `languageField` selection (e.g.
63
64
  // a `select` next to the code field) wins over the schema's static