@byline/admin 3.13.2 → 3.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fields/array/array-field.js +6 -2
- package/dist/fields/blocks/blocks-field.js +6 -2
- package/dist/fields/field-renderer.js +10 -0
- package/dist/fields/relation/relation-field.module.js +16 -1
- package/dist/fields/relation/relation-field_module.css +64 -0
- package/dist/fields/relation/relation-many-field.d.ts +16 -0
- package/dist/fields/relation/relation-many-field.js +193 -0
- package/dist/modules/admin-account/components/theme.d.ts +13 -0
- package/dist/modules/admin-account/components/theme.js +6 -1
- package/dist/react.d.ts +1 -0
- package/dist/react.js +1 -0
- package/package.json +10 -5
- package/src/fields/array/array-field.tsx +11 -3
- package/src/fields/blocks/blocks-field.tsx +11 -3
- package/src/fields/field-renderer.tsx +11 -0
- package/src/fields/relation/relation-field.module.css +81 -0
- package/src/fields/relation/relation-many-field.tsx +274 -0
- package/src/modules/admin-account/components/theme.ts +18 -0
- package/src/react.ts +1 -0
|
@@ -13,12 +13,16 @@ const ArrayField = ({ field, defaultValue, path, disableSorting = false, content
|
|
|
13
13
|
const { t } = useTranslation('byline-admin');
|
|
14
14
|
const [items, setItems] = useState([]);
|
|
15
15
|
useEffect(()=>{
|
|
16
|
-
|
|
16
|
+
const storeValue = getFieldValue(path);
|
|
17
|
+
const source = Array.isArray(storeValue) ? storeValue : defaultValue;
|
|
18
|
+
Array.isArray(source) ? setItems(source.map((item)=>({
|
|
17
19
|
id: item && 'object' == typeof item && 'id' in item ? String(item.id) : item && 'object' == typeof item && '_id' in item ? String(item._id) : crypto.randomUUID(),
|
|
18
20
|
data: item
|
|
19
21
|
}))) : setItems([]);
|
|
20
22
|
}, [
|
|
21
|
-
defaultValue
|
|
23
|
+
defaultValue,
|
|
24
|
+
getFieldValue,
|
|
25
|
+
path
|
|
22
26
|
]);
|
|
23
27
|
const handleDragEnd = ({ moveFromIndex, moveToIndex })=>{
|
|
24
28
|
setItems((prev)=>moveItem(prev, moveFromIndex, moveToIndex));
|
|
@@ -25,12 +25,16 @@ const BlocksField = ({ field, defaultValue, path, contentLocale })=>{
|
|
|
25
25
|
selectedBlockName
|
|
26
26
|
]);
|
|
27
27
|
useEffect(()=>{
|
|
28
|
-
|
|
28
|
+
const storeValue = getFieldValue(path);
|
|
29
|
+
const source = Array.isArray(storeValue) ? storeValue : defaultValue;
|
|
30
|
+
Array.isArray(source) ? setItems(source.map((item)=>({
|
|
29
31
|
id: item && 'object' == typeof item && '_id' in item ? String(item._id) : crypto.randomUUID(),
|
|
30
32
|
data: item
|
|
31
33
|
}))) : setItems([]);
|
|
32
34
|
}, [
|
|
33
|
-
defaultValue
|
|
35
|
+
defaultValue,
|
|
36
|
+
getFieldValue,
|
|
37
|
+
path
|
|
34
38
|
]);
|
|
35
39
|
const handleDragEnd = ({ moveFromIndex, moveToIndex })=>{
|
|
36
40
|
setItems((prev)=>moveItem(prev, moveFromIndex, moveToIndex));
|
|
@@ -12,6 +12,7 @@ import { ImageField } from "./image/image-field.js";
|
|
|
12
12
|
import { LocaleBadge } from "./locale-badge.js";
|
|
13
13
|
import { NumericalField } from "./numerical/numerical-field.js";
|
|
14
14
|
import { RelationField } from "./relation/relation-field.js";
|
|
15
|
+
import { RelationManyField } from "./relation/relation-many-field.js";
|
|
15
16
|
import { SelectField } from "./select/select-field.js";
|
|
16
17
|
import { TextField } from "./text/text-field.js";
|
|
17
18
|
import { TextAreaField } from "./text-area/text-area-field.js";
|
|
@@ -152,6 +153,15 @@ const FieldRenderer = ({ field, defaultValue, basePath, disableSorting, hideLabe
|
|
|
152
153
|
collectionPath: collectionPath
|
|
153
154
|
});
|
|
154
155
|
case 'relation':
|
|
156
|
+
if (field.hasMany) return /*#__PURE__*/ jsx(RelationManyField, {
|
|
157
|
+
field: hideLabel ? {
|
|
158
|
+
...field,
|
|
159
|
+
label: void 0
|
|
160
|
+
} : field,
|
|
161
|
+
defaultValue: defaultValue,
|
|
162
|
+
path: path,
|
|
163
|
+
id: htmlId
|
|
164
|
+
});
|
|
155
165
|
return /*#__PURE__*/ jsx(RelationField, {
|
|
156
166
|
field: hideLabel ? {
|
|
157
167
|
...field,
|
|
@@ -8,6 +8,21 @@ const relation_field_module = {
|
|
|
8
8
|
errorText: "error-text-jHQh34",
|
|
9
9
|
tile: "tile-Y3_yre",
|
|
10
10
|
actions: "actions-Nov8hS",
|
|
11
|
-
mono: "mono-y8Xo6b"
|
|
11
|
+
mono: "mono-y8Xo6b",
|
|
12
|
+
many: "many-ohEmr7",
|
|
13
|
+
"many-empty": "many-empty-T0OJd9",
|
|
14
|
+
manyEmpty: "many-empty-T0OJd9",
|
|
15
|
+
"many-tile": "many-tile-jNifSf",
|
|
16
|
+
manyTile: "many-tile-jNifSf",
|
|
17
|
+
"many-tile-dragging": "many-tile-dragging-AUDjxQ",
|
|
18
|
+
manyTileDragging: "many-tile-dragging-AUDjxQ",
|
|
19
|
+
"many-grip": "many-grip-i7LGT9",
|
|
20
|
+
manyGrip: "many-grip-i7LGT9",
|
|
21
|
+
"many-body": "many-body-e8JKV8",
|
|
22
|
+
manyBody: "many-body-e8JKV8",
|
|
23
|
+
"many-remove": "many-remove-fuuKiO",
|
|
24
|
+
manyRemove: "many-remove-fuuKiO",
|
|
25
|
+
"many-add": "many-add-c5uQ7B",
|
|
26
|
+
manyAdd: "many-add-c5uQ7B"
|
|
12
27
|
};
|
|
13
28
|
export default relation_field_module;
|
|
@@ -60,3 +60,67 @@
|
|
|
60
60
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
:is(.many-ohEmr7, .byline-field-relation-many) {
|
|
64
|
+
gap: var(--spacing-4);
|
|
65
|
+
flex-direction: column;
|
|
66
|
+
margin-top: .25rem;
|
|
67
|
+
display: flex;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
:is(.many-empty-T0OJd9, .byline-field-relation-many-empty) {
|
|
71
|
+
color: var(--gray-400);
|
|
72
|
+
font-size: var(--font-size-xs);
|
|
73
|
+
margin: .25rem 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
:is(.many-tile-jNifSf, .byline-field-relation-many-tile) {
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: var(--spacing-8);
|
|
79
|
+
padding: var(--spacing-4) var(--spacing-8);
|
|
80
|
+
border: var(--border-width-thin) var(--border-style-solid) var(--primary-500);
|
|
81
|
+
border-radius: var(--border-radius-md);
|
|
82
|
+
background-color: var(--surface-100, transparent);
|
|
83
|
+
color: var(--gray-200);
|
|
84
|
+
font-size: var(--font-size-xs);
|
|
85
|
+
display: flex;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
:is(.many-tile-dragging-AUDjxQ, .byline-field-relation-many-tile-dragging) {
|
|
89
|
+
opacity: .85;
|
|
90
|
+
box-shadow: var(--shadow-md, 0 4px 12px #0003);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
:is(.many-grip-i7LGT9, .byline-field-relation-many-grip) {
|
|
94
|
+
color: var(--gray-400);
|
|
95
|
+
cursor: grab;
|
|
96
|
+
touch-action: none;
|
|
97
|
+
background: none;
|
|
98
|
+
border: none;
|
|
99
|
+
flex-shrink: 0;
|
|
100
|
+
align-items: center;
|
|
101
|
+
padding: 0;
|
|
102
|
+
display: inline-flex;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.many-grip-i7LGT9:active {
|
|
106
|
+
cursor: grabbing;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.byline-field-relation-many-grip:active {
|
|
110
|
+
cursor: grabbing;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
:is(.many-body-e8JKV8, .byline-field-relation-many-body) {
|
|
114
|
+
flex: auto;
|
|
115
|
+
min-width: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
:is(.many-remove-fuuKiO, .byline-field-relation-many-remove) {
|
|
119
|
+
flex-shrink: 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
:is(.many-add-c5uQ7B, .byline-field-relation-many-add) {
|
|
123
|
+
margin-top: .25rem;
|
|
124
|
+
display: flex;
|
|
125
|
+
}
|
|
126
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
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 { RelationField as FieldType, RelatedDocumentValue } from '@byline/core';
|
|
9
|
+
interface RelationManyFieldProps {
|
|
10
|
+
field: FieldType;
|
|
11
|
+
defaultValue?: RelatedDocumentValue[] | null;
|
|
12
|
+
id?: string;
|
|
13
|
+
path?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare const RelationManyField: ({ field, defaultValue, id, path }: RelationManyFieldProps) => import("react").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { getCollectionAdminConfig, getCollectionDefinition } from "@byline/core";
|
|
4
|
+
import { useTranslation } from "@byline/i18n/react";
|
|
5
|
+
import { Button, CloseIcon, DraggableSortable, ErrorText, GripperVerticalIcon, IconButton, Label, PlusIcon, moveItem, useSortable } from "@byline/ui/react";
|
|
6
|
+
import classnames from "classnames";
|
|
7
|
+
import { useFieldError, useFieldValue, useFormContext } from "../../forms/form-context.js";
|
|
8
|
+
import relation_field_module from "./relation-field.module.js";
|
|
9
|
+
import { RelationPicker } from "./relation-picker.js";
|
|
10
|
+
import { RelationSummary } from "./relation-summary.js";
|
|
11
|
+
const RelationManyTile = ({ id, children, onRemove, removeAriaLabel })=>{
|
|
12
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
13
|
+
id,
|
|
14
|
+
transition: {
|
|
15
|
+
duration: 250,
|
|
16
|
+
easing: 'cubic-bezier(0, 0.2, 0.2, 1)'
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
const style = {
|
|
20
|
+
transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : void 0,
|
|
21
|
+
transition,
|
|
22
|
+
zIndex: isDragging ? 10 : 'auto'
|
|
23
|
+
};
|
|
24
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
25
|
+
ref: setNodeRef,
|
|
26
|
+
style: style,
|
|
27
|
+
className: classnames('byline-field-relation-many-tile', relation_field_module["many-tile"], isDragging && [
|
|
28
|
+
'byline-field-relation-many-tile-dragging',
|
|
29
|
+
relation_field_module["many-tile-dragging"]
|
|
30
|
+
]),
|
|
31
|
+
children: [
|
|
32
|
+
/*#__PURE__*/ jsx("button", {
|
|
33
|
+
type: "button",
|
|
34
|
+
className: classnames('byline-field-relation-many-grip', relation_field_module["many-grip"]),
|
|
35
|
+
...attributes,
|
|
36
|
+
...listeners,
|
|
37
|
+
children: /*#__PURE__*/ jsx(GripperVerticalIcon, {})
|
|
38
|
+
}),
|
|
39
|
+
/*#__PURE__*/ jsx("div", {
|
|
40
|
+
className: classnames('byline-field-relation-many-body', relation_field_module["many-body"]),
|
|
41
|
+
children: children
|
|
42
|
+
}),
|
|
43
|
+
/*#__PURE__*/ jsx(IconButton, {
|
|
44
|
+
type: "button",
|
|
45
|
+
size: "xs",
|
|
46
|
+
intent: "noeffect",
|
|
47
|
+
"aria-label": removeAriaLabel,
|
|
48
|
+
className: classnames('byline-field-relation-many-remove', relation_field_module["many-remove"]),
|
|
49
|
+
onClick: onRemove,
|
|
50
|
+
children: /*#__PURE__*/ jsx(CloseIcon, {
|
|
51
|
+
width: "14px",
|
|
52
|
+
height: "14px"
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
]
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
59
|
+
const fieldPath = path ?? field.name;
|
|
60
|
+
const htmlId = id ?? fieldPath;
|
|
61
|
+
const { t } = useTranslation('byline-admin');
|
|
62
|
+
const fieldError = useFieldError(fieldPath);
|
|
63
|
+
const { getFieldValue, setFieldValue } = useFormContext();
|
|
64
|
+
const targetDef = getCollectionDefinition(field.targetCollection);
|
|
65
|
+
const targetAdminConfig = getCollectionAdminConfig(field.targetCollection);
|
|
66
|
+
const isUnknown = null == targetDef;
|
|
67
|
+
const targetLabel = targetDef?.labels.singular ?? field.targetCollection;
|
|
68
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
69
|
+
const [pickedRecords, setPickedRecords] = useState({});
|
|
70
|
+
const liveValue = useFieldValue(fieldPath);
|
|
71
|
+
const items = Array.isArray(liveValue) ? liveValue : Array.isArray(defaultValue) ? defaultValue : [];
|
|
72
|
+
const currentArray = ()=>{
|
|
73
|
+
const v = getFieldValue(fieldPath);
|
|
74
|
+
if (Array.isArray(v)) return v;
|
|
75
|
+
return Array.isArray(defaultValue) ? defaultValue : [];
|
|
76
|
+
};
|
|
77
|
+
const handleAdd = (selection)=>{
|
|
78
|
+
setPickerOpen(false);
|
|
79
|
+
const current = currentArray();
|
|
80
|
+
if (current.some((v)=>v.targetDocumentId === selection.targetDocumentId)) return;
|
|
81
|
+
if (selection.record) setPickedRecords((prev)=>({
|
|
82
|
+
...prev,
|
|
83
|
+
[selection.targetDocumentId]: selection.record
|
|
84
|
+
}));
|
|
85
|
+
setFieldValue(fieldPath, [
|
|
86
|
+
...current,
|
|
87
|
+
{
|
|
88
|
+
targetDocumentId: selection.targetDocumentId,
|
|
89
|
+
targetCollectionId: selection.targetCollectionId
|
|
90
|
+
}
|
|
91
|
+
]);
|
|
92
|
+
};
|
|
93
|
+
const handleRemove = (targetDocumentId)=>{
|
|
94
|
+
setFieldValue(fieldPath, currentArray().filter((v)=>v.targetDocumentId !== targetDocumentId));
|
|
95
|
+
};
|
|
96
|
+
const handleDragEnd = ({ moveFromIndex, moveToIndex })=>{
|
|
97
|
+
if (moveFromIndex === moveToIndex) return;
|
|
98
|
+
setFieldValue(fieldPath, moveItem(currentArray(), moveFromIndex, moveToIndex));
|
|
99
|
+
};
|
|
100
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
101
|
+
className: `byline-field-relation ${field.name}`,
|
|
102
|
+
children: [
|
|
103
|
+
/*#__PURE__*/ jsx("div", {
|
|
104
|
+
className: classnames('byline-field-relation-header', relation_field_module.header),
|
|
105
|
+
children: /*#__PURE__*/ jsx(Label, {
|
|
106
|
+
id: `${htmlId}-label`,
|
|
107
|
+
htmlFor: htmlId,
|
|
108
|
+
label: field.label ?? field.name,
|
|
109
|
+
required: !field.optional
|
|
110
|
+
})
|
|
111
|
+
}),
|
|
112
|
+
field.helpText && /*#__PURE__*/ jsx("div", {
|
|
113
|
+
className: classnames('byline-field-relation-help', relation_field_module.help),
|
|
114
|
+
children: field.helpText
|
|
115
|
+
}),
|
|
116
|
+
isUnknown ? /*#__PURE__*/ jsxs("div", {
|
|
117
|
+
className: classnames('byline-field-relation-error-tile', relation_field_module["error-tile"]),
|
|
118
|
+
children: [
|
|
119
|
+
/*#__PURE__*/ jsx("span", {
|
|
120
|
+
children: t('fields.relation.unknownError', {
|
|
121
|
+
name: field.name,
|
|
122
|
+
target: field.targetCollection
|
|
123
|
+
})
|
|
124
|
+
}),
|
|
125
|
+
/*#__PURE__*/ jsx("span", {
|
|
126
|
+
className: classnames('byline-field-relation-error-text', relation_field_module["error-text"]),
|
|
127
|
+
children: t('fields.relation.unknownHint')
|
|
128
|
+
})
|
|
129
|
+
]
|
|
130
|
+
}) : /*#__PURE__*/ jsxs(Fragment, {
|
|
131
|
+
children: [
|
|
132
|
+
0 === items.length ? /*#__PURE__*/ jsx("p", {
|
|
133
|
+
className: classnames('byline-field-relation-many-empty', relation_field_module["many-empty"]),
|
|
134
|
+
children: t('fields.relation.manyEmpty', {
|
|
135
|
+
label: targetDef.labels.plural
|
|
136
|
+
})
|
|
137
|
+
}) : /*#__PURE__*/ jsx(DraggableSortable, {
|
|
138
|
+
ids: items.map((v)=>v.targetDocumentId),
|
|
139
|
+
onDragEnd: handleDragEnd,
|
|
140
|
+
className: classnames('byline-field-relation-many', relation_field_module.many),
|
|
141
|
+
children: items.map((value)=>/*#__PURE__*/ jsx(RelationManyTile, {
|
|
142
|
+
id: value.targetDocumentId,
|
|
143
|
+
onRemove: ()=>handleRemove(value.targetDocumentId),
|
|
144
|
+
removeAriaLabel: t('fields.relation.removeAriaLabel', {
|
|
145
|
+
label: targetLabel
|
|
146
|
+
}),
|
|
147
|
+
children: /*#__PURE__*/ jsx(RelationSummary, {
|
|
148
|
+
targetDefinition: targetDef,
|
|
149
|
+
targetAdminConfig: targetAdminConfig,
|
|
150
|
+
displayField: field.displayField,
|
|
151
|
+
value: value,
|
|
152
|
+
cachedRecord: pickedRecords[value.targetDocumentId] ?? null
|
|
153
|
+
})
|
|
154
|
+
}, value.targetDocumentId))
|
|
155
|
+
}),
|
|
156
|
+
fieldError && /*#__PURE__*/ jsx(ErrorText, {
|
|
157
|
+
id: `${field.name}-error`,
|
|
158
|
+
text: fieldError
|
|
159
|
+
}),
|
|
160
|
+
/*#__PURE__*/ jsx("div", {
|
|
161
|
+
className: classnames('byline-field-relation-many-add', relation_field_module["many-add"]),
|
|
162
|
+
children: /*#__PURE__*/ jsxs(Button, {
|
|
163
|
+
id: htmlId,
|
|
164
|
+
size: "xs",
|
|
165
|
+
variant: "outlined",
|
|
166
|
+
intent: "noeffect",
|
|
167
|
+
type: "button",
|
|
168
|
+
onClick: ()=>setPickerOpen(true),
|
|
169
|
+
children: [
|
|
170
|
+
/*#__PURE__*/ jsx(PlusIcon, {
|
|
171
|
+
width: "14px",
|
|
172
|
+
height: "14px"
|
|
173
|
+
}),
|
|
174
|
+
t('fields.relation.addButton', {
|
|
175
|
+
label: targetLabel
|
|
176
|
+
})
|
|
177
|
+
]
|
|
178
|
+
})
|
|
179
|
+
}),
|
|
180
|
+
/*#__PURE__*/ jsx(RelationPicker, {
|
|
181
|
+
targetCollectionPath: field.targetCollection,
|
|
182
|
+
targetDefinition: targetDef,
|
|
183
|
+
displayField: field.displayField,
|
|
184
|
+
isOpen: pickerOpen,
|
|
185
|
+
onSelect: handleAdd,
|
|
186
|
+
onDismiss: ()=>setPickerOpen(false)
|
|
187
|
+
})
|
|
188
|
+
]
|
|
189
|
+
})
|
|
190
|
+
]
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
export { RelationManyField };
|
|
@@ -33,3 +33,16 @@ export declare function getThemeMode(): ThemeMode;
|
|
|
33
33
|
* override so the document follows `prefers-color-scheme` again.
|
|
34
34
|
*/
|
|
35
35
|
export declare function setThemeMode(mode: ThemeMode): void;
|
|
36
|
+
/**
|
|
37
|
+
* Re-assert the stored theme on the live document without changing it.
|
|
38
|
+
*
|
|
39
|
+
* Byline admin shares the host's theme contract (the `theme` key + the
|
|
40
|
+
* `.dark`/`.light` class on `<html>`), so the surrounding host owns the
|
|
41
|
+
* before-first-paint application. But a host theme provider can re-apply
|
|
42
|
+
* its own (possibly stale, possibly forced) theme on navigation and
|
|
43
|
+
* clobber the admin user's stored choice. Calling this when the admin
|
|
44
|
+
* shell mounts re-applies the stored preference from the single source of
|
|
45
|
+
* truth — `localStorage` — making the admin area self-correcting
|
|
46
|
+
* regardless of how the host manages theme. No-op on the server.
|
|
47
|
+
*/
|
|
48
|
+
export declare function applyStoredTheme(): void;
|
|
@@ -27,4 +27,9 @@ function setThemeMode(mode) {
|
|
|
27
27
|
applyEffectiveTheme(mode);
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
function applyStoredTheme() {
|
|
31
|
+
if ("u" < typeof window) return;
|
|
32
|
+
const mode = getThemeMode();
|
|
33
|
+
applyEffectiveTheme('system' === mode ? systemPrefersDark() ? 'dark' : 'light' : mode);
|
|
34
|
+
}
|
|
35
|
+
export { applyStoredTheme, getThemeMode, setThemeMode };
|
package/dist/react.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export * from './fields/local-date-time.js';
|
|
|
46
46
|
export * from './fields/locale-badge.js';
|
|
47
47
|
export * from './fields/numerical/numerical-field.js';
|
|
48
48
|
export * from './fields/relation/relation-field.js';
|
|
49
|
+
export * from './fields/relation/relation-many-field.js';
|
|
49
50
|
export * from './fields/relation/relation-picker.js';
|
|
50
51
|
export * from './fields/select/select-field.js';
|
|
51
52
|
export * from './fields/sortable-item.js';
|
package/dist/react.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from "./fields/local-date-time.js";
|
|
|
17
17
|
export * from "./fields/locale-badge.js";
|
|
18
18
|
export * from "./fields/numerical/numerical-field.js";
|
|
19
19
|
export * from "./fields/relation/relation-field.js";
|
|
20
|
+
export * from "./fields/relation/relation-many-field.js";
|
|
20
21
|
export * from "./fields/relation/relation-picker.js";
|
|
21
22
|
export * from "./fields/select/select-field.js";
|
|
22
23
|
export * from "./fields/sortable-item.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": "3.
|
|
5
|
+
"version": "3.14.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -122,6 +122,11 @@
|
|
|
122
122
|
"import": "./dist/modules/admin-account/components/container.js",
|
|
123
123
|
"require": "./dist/modules/admin-account/components/container.js"
|
|
124
124
|
},
|
|
125
|
+
"./admin-account/components/theme": {
|
|
126
|
+
"types": "./dist/modules/admin-account/components/theme.d.ts",
|
|
127
|
+
"import": "./dist/modules/admin-account/components/theme.js",
|
|
128
|
+
"require": "./dist/modules/admin-account/components/theme.js"
|
|
129
|
+
},
|
|
125
130
|
"./admin-account/components/update": {
|
|
126
131
|
"types": "./dist/modules/admin-account/components/update.d.ts",
|
|
127
132
|
"import": "./dist/modules/admin-account/components/update.js",
|
|
@@ -151,10 +156,10 @@
|
|
|
151
156
|
"uuid": "^14.0.0",
|
|
152
157
|
"zod": "^4.4.3",
|
|
153
158
|
"zod-form-data": "^3.0.1",
|
|
154
|
-
"@byline/auth": "3.
|
|
155
|
-
"@byline/ui": "3.
|
|
156
|
-
"@byline/core": "3.
|
|
157
|
-
"@byline/i18n": "3.
|
|
159
|
+
"@byline/auth": "3.14.0",
|
|
160
|
+
"@byline/ui": "3.14.0",
|
|
161
|
+
"@byline/core": "3.14.0",
|
|
162
|
+
"@byline/i18n": "3.14.0"
|
|
158
163
|
},
|
|
159
164
|
"peerDependencies": {
|
|
160
165
|
"react": "^19.0.0",
|
|
@@ -47,9 +47,17 @@ export const ArrayField = ({
|
|
|
47
47
|
const [items, setItems] = useState<{ id: string; data: any }[]>([])
|
|
48
48
|
|
|
49
49
|
useEffect(() => {
|
|
50
|
-
|
|
50
|
+
// Prefer the live form-store value over `defaultValue` when seeding the
|
|
51
|
+
// rendered list. The store lives in FormProvider — above the tab layout —
|
|
52
|
+
// so it survives this field unmounting/remounting as the editor switches
|
|
53
|
+
// tabs; `defaultValue` is only the initial seed and is empty in create
|
|
54
|
+
// mode. Re-seeding from `defaultValue` on remount would drop items the
|
|
55
|
+
// editor had already added on an earlier visit to this tab.
|
|
56
|
+
const storeValue = getFieldValue(path)
|
|
57
|
+
const source = Array.isArray(storeValue) ? storeValue : defaultValue
|
|
58
|
+
if (Array.isArray(source)) {
|
|
51
59
|
setItems(
|
|
52
|
-
|
|
60
|
+
source.map((item: any) => ({
|
|
53
61
|
id:
|
|
54
62
|
item && typeof item === 'object' && 'id' in item
|
|
55
63
|
? String((item as { id: string }).id)
|
|
@@ -62,7 +70,7 @@ export const ArrayField = ({
|
|
|
62
70
|
} else {
|
|
63
71
|
setItems([])
|
|
64
72
|
}
|
|
65
|
-
}, [defaultValue])
|
|
73
|
+
}, [defaultValue, getFieldValue, path])
|
|
66
74
|
|
|
67
75
|
const handleDragEnd = ({
|
|
68
76
|
moveFromIndex,
|
|
@@ -73,9 +73,17 @@ export const BlocksField = ({
|
|
|
73
73
|
}, [availableBlocks, selectedBlockName])
|
|
74
74
|
|
|
75
75
|
useEffect(() => {
|
|
76
|
-
|
|
76
|
+
// Prefer the live form-store value over `defaultValue` when seeding the
|
|
77
|
+
// rendered list. The store lives in FormProvider — above the tab layout —
|
|
78
|
+
// so it survives this field unmounting/remounting as the editor switches
|
|
79
|
+
// tabs; `defaultValue` is only the initial seed and is empty in create
|
|
80
|
+
// mode. Re-seeding from `defaultValue` on remount would drop blocks the
|
|
81
|
+
// editor had already added on an earlier visit to this tab.
|
|
82
|
+
const storeValue = getFieldValue(path)
|
|
83
|
+
const source = Array.isArray(storeValue) ? storeValue : defaultValue
|
|
84
|
+
if (Array.isArray(source)) {
|
|
77
85
|
setItems(
|
|
78
|
-
|
|
86
|
+
source.map((item: any) => ({
|
|
79
87
|
id:
|
|
80
88
|
item && typeof item === 'object' && '_id' in item
|
|
81
89
|
? String((item as { _id: string })._id)
|
|
@@ -86,7 +94,7 @@ export const BlocksField = ({
|
|
|
86
94
|
} else {
|
|
87
95
|
setItems([])
|
|
88
96
|
}
|
|
89
|
-
}, [defaultValue])
|
|
97
|
+
}, [defaultValue, getFieldValue, path])
|
|
90
98
|
|
|
91
99
|
const handleDragEnd = ({
|
|
92
100
|
moveFromIndex,
|
|
@@ -28,6 +28,7 @@ import { ImageField } from './image/image-field'
|
|
|
28
28
|
import { LocaleBadge } from './locale-badge'
|
|
29
29
|
import { NumericalField } from './numerical/numerical-field'
|
|
30
30
|
import { RelationField } from './relation/relation-field'
|
|
31
|
+
import { RelationManyField } from './relation/relation-many-field'
|
|
31
32
|
import { SelectField } from './select/select-field'
|
|
32
33
|
import { TextField } from './text/text-field'
|
|
33
34
|
import { TextAreaField } from './text-area/text-area-field'
|
|
@@ -223,6 +224,16 @@ export const FieldRenderer = ({
|
|
|
223
224
|
/>
|
|
224
225
|
)
|
|
225
226
|
case 'relation':
|
|
227
|
+
if (field.hasMany) {
|
|
228
|
+
return (
|
|
229
|
+
<RelationManyField
|
|
230
|
+
field={hideLabel ? { ...field, label: undefined } : field}
|
|
231
|
+
defaultValue={defaultValue}
|
|
232
|
+
path={path}
|
|
233
|
+
id={htmlId}
|
|
234
|
+
/>
|
|
235
|
+
)
|
|
236
|
+
}
|
|
226
237
|
return (
|
|
227
238
|
<RelationField
|
|
228
239
|
field={hideLabel ? { ...field, label: undefined } : field}
|
|
@@ -81,3 +81,84 @@
|
|
|
81
81
|
:global(.byline-field-relation-mono) {
|
|
82
82
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
/* ---------------------------------------------------------------------------
|
|
86
|
+
* hasMany — ordered list of relation tiles with drag-reorder + remove.
|
|
87
|
+
*
|
|
88
|
+
* Override handles:
|
|
89
|
+
* .byline-field-relation-many — vertical list of tiles
|
|
90
|
+
* .byline-field-relation-many-empty — empty-state hint
|
|
91
|
+
* .byline-field-relation-many-tile — one row: grip + summary + remove
|
|
92
|
+
* .byline-field-relation-many-grip — drag handle
|
|
93
|
+
* .byline-field-relation-many-body — flexible summary cell
|
|
94
|
+
* .byline-field-relation-many-add — add-item button row
|
|
95
|
+
* --------------------------------------------------------------------------- */
|
|
96
|
+
|
|
97
|
+
.many,
|
|
98
|
+
:global(.byline-field-relation-many) {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: var(--spacing-4);
|
|
102
|
+
margin-top: 0.25rem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.many-empty,
|
|
106
|
+
:global(.byline-field-relation-many-empty) {
|
|
107
|
+
margin: 0.25rem 0;
|
|
108
|
+
color: var(--gray-400);
|
|
109
|
+
font-size: var(--font-size-xs);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.many-tile,
|
|
113
|
+
:global(.byline-field-relation-many-tile) {
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: var(--spacing-8);
|
|
117
|
+
padding: var(--spacing-4) var(--spacing-8);
|
|
118
|
+
border: var(--border-width-thin) var(--border-style-solid) var(--primary-500);
|
|
119
|
+
border-radius: var(--border-radius-md);
|
|
120
|
+
background-color: var(--surface-100, transparent);
|
|
121
|
+
color: var(--gray-200);
|
|
122
|
+
font-size: var(--font-size-xs);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.many-tile-dragging,
|
|
126
|
+
:global(.byline-field-relation-many-tile-dragging) {
|
|
127
|
+
opacity: 0.85;
|
|
128
|
+
box-shadow: var(--shadow-md, 0 4px 12px rgb(0 0 0 / 0.2));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.many-grip,
|
|
132
|
+
:global(.byline-field-relation-many-grip) {
|
|
133
|
+
display: inline-flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
padding: 0;
|
|
137
|
+
border: none;
|
|
138
|
+
background: none;
|
|
139
|
+
color: var(--gray-400);
|
|
140
|
+
cursor: grab;
|
|
141
|
+
touch-action: none;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.many-grip:active,
|
|
145
|
+
:global(.byline-field-relation-many-grip:active) {
|
|
146
|
+
cursor: grabbing;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.many-body,
|
|
150
|
+
:global(.byline-field-relation-many-body) {
|
|
151
|
+
flex: 1 1 auto;
|
|
152
|
+
min-width: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.many-remove,
|
|
156
|
+
:global(.byline-field-relation-many-remove) {
|
|
157
|
+
flex-shrink: 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.many-add,
|
|
161
|
+
:global(.byline-field-relation-many-add) {
|
|
162
|
+
display: flex;
|
|
163
|
+
margin-top: 0.25rem;
|
|
164
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
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
|
+
import { type ReactNode, useState } from 'react'
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
CollectionAdminConfig,
|
|
13
|
+
CollectionDefinition,
|
|
14
|
+
RelationField as FieldType,
|
|
15
|
+
RelatedDocumentValue,
|
|
16
|
+
} from '@byline/core'
|
|
17
|
+
import { getCollectionAdminConfig, getCollectionDefinition } from '@byline/core'
|
|
18
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
19
|
+
import {
|
|
20
|
+
Button,
|
|
21
|
+
CloseIcon,
|
|
22
|
+
DraggableSortable,
|
|
23
|
+
ErrorText,
|
|
24
|
+
GripperVerticalIcon,
|
|
25
|
+
IconButton,
|
|
26
|
+
Label,
|
|
27
|
+
moveItem,
|
|
28
|
+
PlusIcon,
|
|
29
|
+
useSortable,
|
|
30
|
+
} from '@byline/ui/react'
|
|
31
|
+
import cx from 'classnames'
|
|
32
|
+
|
|
33
|
+
import { useFieldError, useFieldValue, useFormContext } from '../../forms/form-context'
|
|
34
|
+
import styles from './relation-field.module.css'
|
|
35
|
+
import { RelationPicker } from './relation-picker'
|
|
36
|
+
import { RelationSummary } from './relation-summary'
|
|
37
|
+
|
|
38
|
+
// A single stored item. On the edit path the loader's populate pass attaches
|
|
39
|
+
// `_resolved` / `document`; freshly-picked items are bare refs. Both flatten
|
|
40
|
+
// the same way (storage ignores the envelope keys), so we never strip them.
|
|
41
|
+
type IncomingRelationValue = RelatedDocumentValue & {
|
|
42
|
+
_resolved?: boolean
|
|
43
|
+
_cycle?: boolean
|
|
44
|
+
document?: Record<string, any>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// RelationManyField — `type: 'relation'` with `hasMany: true`.
|
|
49
|
+
//
|
|
50
|
+
// An ordered list of summary tiles: drag to reorder, ✕ to remove, "+ Add" to
|
|
51
|
+
// open the standard picker and append. The value is an array of relation
|
|
52
|
+
// envelopes; every mutation writes the whole array back via `setFieldValue`,
|
|
53
|
+
// which emits a coalesced `field.set` patch — relations carry no stable `_id`,
|
|
54
|
+
// so the granular `array.*` patches (which key on `_id`) don't apply, and a
|
|
55
|
+
// whole-array set is both correct and simplest for these small lists.
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
interface RelationManyTileProps {
|
|
59
|
+
id: string
|
|
60
|
+
children: ReactNode
|
|
61
|
+
onRemove: () => void
|
|
62
|
+
removeAriaLabel: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const RelationManyTile = ({ id, children, onRemove, removeAriaLabel }: RelationManyTileProps) => {
|
|
66
|
+
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
|
67
|
+
id,
|
|
68
|
+
transition: { duration: 250, easing: 'cubic-bezier(0, 0.2, 0.2, 1)' },
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const style = {
|
|
72
|
+
transform: transform ? `translate3d(${transform.x}px, ${transform.y}px, 0)` : undefined,
|
|
73
|
+
transition,
|
|
74
|
+
zIndex: isDragging ? 10 : 'auto',
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
ref={setNodeRef}
|
|
80
|
+
style={style}
|
|
81
|
+
className={cx(
|
|
82
|
+
'byline-field-relation-many-tile',
|
|
83
|
+
styles['many-tile'],
|
|
84
|
+
isDragging && ['byline-field-relation-many-tile-dragging', styles['many-tile-dragging']]
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className={cx('byline-field-relation-many-grip', styles['many-grip'])}
|
|
90
|
+
{...attributes}
|
|
91
|
+
{...listeners}
|
|
92
|
+
>
|
|
93
|
+
<GripperVerticalIcon />
|
|
94
|
+
</button>
|
|
95
|
+
<div className={cx('byline-field-relation-many-body', styles['many-body'])}>{children}</div>
|
|
96
|
+
<IconButton
|
|
97
|
+
type="button"
|
|
98
|
+
size="xs"
|
|
99
|
+
intent="noeffect"
|
|
100
|
+
aria-label={removeAriaLabel}
|
|
101
|
+
className={cx('byline-field-relation-many-remove', styles['many-remove'])}
|
|
102
|
+
onClick={onRemove}
|
|
103
|
+
>
|
|
104
|
+
<CloseIcon width="14px" height="14px" />
|
|
105
|
+
</IconButton>
|
|
106
|
+
</div>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface RelationManyFieldProps {
|
|
111
|
+
field: FieldType
|
|
112
|
+
defaultValue?: RelatedDocumentValue[] | null
|
|
113
|
+
id?: string
|
|
114
|
+
path?: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const RelationManyField = ({ field, defaultValue, id, path }: RelationManyFieldProps) => {
|
|
118
|
+
const fieldPath = path ?? field.name
|
|
119
|
+
const htmlId = id ?? fieldPath
|
|
120
|
+
const { t } = useTranslation('byline-admin')
|
|
121
|
+
const fieldError = useFieldError(fieldPath)
|
|
122
|
+
const { getFieldValue, setFieldValue } = useFormContext()
|
|
123
|
+
|
|
124
|
+
const targetDef: CollectionDefinition | null = getCollectionDefinition(field.targetCollection)
|
|
125
|
+
const targetAdminConfig: CollectionAdminConfig | null = getCollectionAdminConfig(
|
|
126
|
+
field.targetCollection
|
|
127
|
+
)
|
|
128
|
+
const isUnknown = targetDef == null
|
|
129
|
+
const targetLabel = targetDef?.labels.singular ?? field.targetCollection
|
|
130
|
+
|
|
131
|
+
const [pickerOpen, setPickerOpen] = useState(false)
|
|
132
|
+
// Records handed back by the picker, keyed by target id — lets a freshly
|
|
133
|
+
// added tile render real display data (title / thumbnail) without a refetch.
|
|
134
|
+
const [pickedRecords, setPickedRecords] = useState<Record<string, Record<string, any>>>({})
|
|
135
|
+
|
|
136
|
+
// Subscribe to the live array so reorders / adds / removes re-render.
|
|
137
|
+
const liveValue = useFieldValue<IncomingRelationValue[] | undefined>(fieldPath)
|
|
138
|
+
const items: IncomingRelationValue[] = Array.isArray(liveValue)
|
|
139
|
+
? liveValue
|
|
140
|
+
: Array.isArray(defaultValue)
|
|
141
|
+
? (defaultValue as IncomingRelationValue[])
|
|
142
|
+
: []
|
|
143
|
+
|
|
144
|
+
const currentArray = (): IncomingRelationValue[] => {
|
|
145
|
+
const v = getFieldValue(fieldPath)
|
|
146
|
+
if (Array.isArray(v)) return v as IncomingRelationValue[]
|
|
147
|
+
return Array.isArray(defaultValue) ? (defaultValue as IncomingRelationValue[]) : []
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const handleAdd = (selection: {
|
|
151
|
+
targetDocumentId: string
|
|
152
|
+
targetCollectionId: string
|
|
153
|
+
record?: Record<string, any>
|
|
154
|
+
}) => {
|
|
155
|
+
setPickerOpen(false)
|
|
156
|
+
const current = currentArray()
|
|
157
|
+
// Dedup — a target may appear at most once.
|
|
158
|
+
if (current.some((v) => v.targetDocumentId === selection.targetDocumentId)) return
|
|
159
|
+
if (selection.record) {
|
|
160
|
+
setPickedRecords((prev) => ({ ...prev, [selection.targetDocumentId]: selection.record! }))
|
|
161
|
+
}
|
|
162
|
+
setFieldValue(fieldPath, [
|
|
163
|
+
...current,
|
|
164
|
+
{
|
|
165
|
+
targetDocumentId: selection.targetDocumentId,
|
|
166
|
+
targetCollectionId: selection.targetCollectionId,
|
|
167
|
+
},
|
|
168
|
+
])
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const handleRemove = (targetDocumentId: string) => {
|
|
172
|
+
setFieldValue(
|
|
173
|
+
fieldPath,
|
|
174
|
+
currentArray().filter((v) => v.targetDocumentId !== targetDocumentId)
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const handleDragEnd = ({
|
|
179
|
+
moveFromIndex,
|
|
180
|
+
moveToIndex,
|
|
181
|
+
}: {
|
|
182
|
+
moveFromIndex: number
|
|
183
|
+
moveToIndex: number
|
|
184
|
+
}) => {
|
|
185
|
+
if (moveFromIndex === moveToIndex) return
|
|
186
|
+
setFieldValue(fieldPath, moveItem(currentArray(), moveFromIndex, moveToIndex))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
<div className={`byline-field-relation ${field.name}`}>
|
|
191
|
+
<div className={cx('byline-field-relation-header', styles.header)}>
|
|
192
|
+
<Label
|
|
193
|
+
id={`${htmlId}-label`}
|
|
194
|
+
htmlFor={htmlId}
|
|
195
|
+
label={field.label ?? field.name}
|
|
196
|
+
required={!field.optional}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
199
|
+
{field.helpText && (
|
|
200
|
+
<div className={cx('byline-field-relation-help', styles.help)}>{field.helpText}</div>
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{isUnknown ? (
|
|
204
|
+
<div className={cx('byline-field-relation-error-tile', styles['error-tile'])}>
|
|
205
|
+
<span>
|
|
206
|
+
{t('fields.relation.unknownError', {
|
|
207
|
+
name: field.name,
|
|
208
|
+
target: field.targetCollection,
|
|
209
|
+
})}
|
|
210
|
+
</span>
|
|
211
|
+
<span className={cx('byline-field-relation-error-text', styles['error-text'])}>
|
|
212
|
+
{t('fields.relation.unknownHint')}
|
|
213
|
+
</span>
|
|
214
|
+
</div>
|
|
215
|
+
) : (
|
|
216
|
+
<>
|
|
217
|
+
{items.length === 0 ? (
|
|
218
|
+
<p className={cx('byline-field-relation-many-empty', styles['many-empty'])}>
|
|
219
|
+
{t('fields.relation.manyEmpty', { label: targetDef.labels.plural })}
|
|
220
|
+
</p>
|
|
221
|
+
) : (
|
|
222
|
+
<DraggableSortable
|
|
223
|
+
ids={items.map((v) => v.targetDocumentId)}
|
|
224
|
+
onDragEnd={handleDragEnd}
|
|
225
|
+
className={cx('byline-field-relation-many', styles.many)}
|
|
226
|
+
>
|
|
227
|
+
{items.map((value) => (
|
|
228
|
+
<RelationManyTile
|
|
229
|
+
key={value.targetDocumentId}
|
|
230
|
+
id={value.targetDocumentId}
|
|
231
|
+
onRemove={() => handleRemove(value.targetDocumentId)}
|
|
232
|
+
removeAriaLabel={t('fields.relation.removeAriaLabel', { label: targetLabel })}
|
|
233
|
+
>
|
|
234
|
+
<RelationSummary
|
|
235
|
+
targetDefinition={targetDef}
|
|
236
|
+
targetAdminConfig={targetAdminConfig}
|
|
237
|
+
displayField={field.displayField}
|
|
238
|
+
value={value}
|
|
239
|
+
cachedRecord={pickedRecords[value.targetDocumentId] ?? null}
|
|
240
|
+
/>
|
|
241
|
+
</RelationManyTile>
|
|
242
|
+
))}
|
|
243
|
+
</DraggableSortable>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
{fieldError && <ErrorText id={`${field.name}-error`} text={fieldError} />}
|
|
247
|
+
|
|
248
|
+
<div className={cx('byline-field-relation-many-add', styles['many-add'])}>
|
|
249
|
+
<Button
|
|
250
|
+
id={htmlId}
|
|
251
|
+
size="xs"
|
|
252
|
+
variant="outlined"
|
|
253
|
+
intent="noeffect"
|
|
254
|
+
type="button"
|
|
255
|
+
onClick={() => setPickerOpen(true)}
|
|
256
|
+
>
|
|
257
|
+
<PlusIcon width="14px" height="14px" />
|
|
258
|
+
{t('fields.relation.addButton', { label: targetLabel })}
|
|
259
|
+
</Button>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<RelationPicker
|
|
263
|
+
targetCollectionPath={field.targetCollection}
|
|
264
|
+
targetDefinition={targetDef}
|
|
265
|
+
displayField={field.displayField}
|
|
266
|
+
isOpen={pickerOpen}
|
|
267
|
+
onSelect={handleAdd}
|
|
268
|
+
onDismiss={() => setPickerOpen(false)}
|
|
269
|
+
/>
|
|
270
|
+
</>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
)
|
|
274
|
+
}
|
|
@@ -70,3 +70,21 @@ export function setThemeMode(mode: ThemeMode): void {
|
|
|
70
70
|
applyEffectiveTheme(mode)
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Re-assert the stored theme on the live document without changing it.
|
|
76
|
+
*
|
|
77
|
+
* Byline admin shares the host's theme contract (the `theme` key + the
|
|
78
|
+
* `.dark`/`.light` class on `<html>`), so the surrounding host owns the
|
|
79
|
+
* before-first-paint application. But a host theme provider can re-apply
|
|
80
|
+
* its own (possibly stale, possibly forced) theme on navigation and
|
|
81
|
+
* clobber the admin user's stored choice. Calling this when the admin
|
|
82
|
+
* shell mounts re-applies the stored preference from the single source of
|
|
83
|
+
* truth — `localStorage` — making the admin area self-correcting
|
|
84
|
+
* regardless of how the host manages theme. No-op on the server.
|
|
85
|
+
*/
|
|
86
|
+
export function applyStoredTheme(): void {
|
|
87
|
+
if (typeof window === 'undefined') return
|
|
88
|
+
const mode = getThemeMode()
|
|
89
|
+
applyEffectiveTheme(mode === 'system' ? (systemPrefersDark() ? 'dark' : 'light') : mode)
|
|
90
|
+
}
|
package/src/react.ts
CHANGED
|
@@ -49,6 +49,7 @@ export * from './fields/local-date-time.js'
|
|
|
49
49
|
export * from './fields/locale-badge.js'
|
|
50
50
|
export * from './fields/numerical/numerical-field.js'
|
|
51
51
|
export * from './fields/relation/relation-field.js'
|
|
52
|
+
export * from './fields/relation/relation-many-field.js'
|
|
52
53
|
export * from './fields/relation/relation-picker.js'
|
|
53
54
|
export * from './fields/select/select-field.js'
|
|
54
55
|
export * from './fields/sortable-item.js'
|