@byline/admin 4.16.0 → 4.17.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/relation/relation-field.js +3 -2
- package/dist/fields/relation/relation-many-field.js +3 -2
- package/dist/fields/relation/relation-picker.d.ts +2 -2
- package/dist/fields/relation/relation-summary.d.ts +2 -2
- package/dist/forms/document-actions.js +30 -25
- package/dist/forms/form-renderer.d.ts +17 -3
- package/dist/forms/form-renderer.js +18 -9
- package/dist/forms/tree-placement-widget.js +3 -2
- package/dist/forms/use-form-layout.d.ts +3 -3
- package/package.json +11 -13
- package/src/fields/relation/relation-field.tsx +5 -3
- package/src/fields/relation/relation-many-field.tsx +5 -3
- package/src/fields/relation/relation-picker.tsx +2 -2
- package/src/fields/relation/relation-summary.tsx +2 -2
- package/src/forms/document-actions.test.tsx +157 -0
- package/src/forms/document-actions.tsx +93 -74
- package/src/forms/form-renderer-capabilities.test.tsx +155 -0
- package/src/forms/form-renderer-submit.test.tsx +194 -0
- package/src/forms/form-renderer.tsx +59 -25
- package/src/forms/tree-placement-widget.tsx +3 -2
- package/src/forms/use-form-layout.ts +3 -3
- package/dist/forms/__probe-nested.test.node.d.ts +0 -1
- package/dist/forms/__probe-two.test.node.d.ts +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import { getCollectionAdminConfig, getCollectionDefinition } from "@byline/core";
|
|
3
|
+
import { getCollectionAdminConfig, getCollectionDefinition, isSingleton } from "@byline/core";
|
|
4
4
|
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";
|
|
@@ -14,7 +14,8 @@ const RelationField = ({ field, value, defaultValue, onChange, id, path })=>{
|
|
|
14
14
|
const fieldError = useFieldError(fieldPath);
|
|
15
15
|
const fieldValue = useFieldValue(fieldPath);
|
|
16
16
|
const incomingValue = void 0 !== fieldValue ? fieldValue ?? null : value ?? defaultValue ?? null;
|
|
17
|
-
const
|
|
17
|
+
const targetDefinition = getCollectionDefinition(field.targetCollection);
|
|
18
|
+
const targetDef = null == targetDefinition || isSingleton(targetDefinition) ? null : targetDefinition;
|
|
18
19
|
const targetAdminConfig = getCollectionAdminConfig(field.targetCollection);
|
|
19
20
|
const { t } = useTranslation('byline-admin');
|
|
20
21
|
const [pickerOpen, setPickerOpen] = useState(false);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import { getCollectionAdminConfig, getCollectionDefinition } from "@byline/core";
|
|
3
|
+
import { getCollectionAdminConfig, getCollectionDefinition, isSingleton } from "@byline/core";
|
|
4
4
|
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";
|
|
@@ -61,7 +61,8 @@ const RelationManyField = ({ field, defaultValue, id, path })=>{
|
|
|
61
61
|
const { t } = useTranslation('byline-admin');
|
|
62
62
|
const fieldError = useFieldError(fieldPath);
|
|
63
63
|
const { getFieldValue, setFieldValue } = useFormContext();
|
|
64
|
-
const
|
|
64
|
+
const targetDefinition = getCollectionDefinition(field.targetCollection);
|
|
65
|
+
const targetDef = null == targetDefinition || isSingleton(targetDefinition) ? null : targetDefinition;
|
|
65
66
|
const targetAdminConfig = getCollectionAdminConfig(field.targetCollection);
|
|
66
67
|
const isUnknown = null == targetDef;
|
|
67
68
|
const targetLabel = targetDef?.labels.singular ?? field.targetCollection;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import type {
|
|
8
|
+
import type { MultiCollectionDefinition } from '@byline/core';
|
|
9
9
|
/**
|
|
10
10
|
* Row rendering strategy, in priority order:
|
|
11
11
|
* 1. `CollectionAdminConfig.itemView` — a ColumnDefinition[] from the target
|
|
@@ -35,7 +35,7 @@ interface RelationPickerBaseProps {
|
|
|
35
35
|
/** The target collection path (e.g. `'media'`). */
|
|
36
36
|
targetCollectionPath: string;
|
|
37
37
|
/** The target collection definition (used for labels + displayField fallback). */
|
|
38
|
-
targetDefinition?:
|
|
38
|
+
targetDefinition?: MultiCollectionDefinition | null;
|
|
39
39
|
/** Explicit display field to render as row label. */
|
|
40
40
|
displayField?: string;
|
|
41
41
|
/**
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
|
-
import type { CollectionAdminConfig,
|
|
8
|
+
import type { CollectionAdminConfig, MultiCollectionDefinition } from '@byline/core';
|
|
9
9
|
interface RelationSummaryProps {
|
|
10
|
-
targetDefinition:
|
|
10
|
+
targetDefinition: MultiCollectionDefinition;
|
|
11
11
|
targetAdminConfig: CollectionAdminConfig | null;
|
|
12
12
|
displayField?: string;
|
|
13
13
|
/** The raw relation value from the form. May be a plain ref or a populated envelope. */
|
|
@@ -49,6 +49,7 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate,
|
|
|
49
49
|
onSelect: ()=>void onCancelScheduledPublication()
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
+
const hasAnyAction = schedulingActions.length > 0 || copyToLocaleAvailable || deleteLocaleAvailable || null != onDuplicate || null != onDelete;
|
|
52
53
|
const handleOnDelete = ()=>{
|
|
53
54
|
setShowDeleteConfirm(false);
|
|
54
55
|
if (onDelete) onDelete();
|
|
@@ -109,7 +110,7 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate,
|
|
|
109
110
|
const sourceLocaleLabel = contentLocales?.find((loc)=>loc.code === sourceLocale)?.label ?? sourceLocale ?? '';
|
|
110
111
|
return /*#__PURE__*/ jsxs(Fragment, {
|
|
111
112
|
children: [
|
|
112
|
-
/*#__PURE__*/ jsxs(Dropdown.Root, {
|
|
113
|
+
hasAnyAction && /*#__PURE__*/ jsxs(Dropdown.Root, {
|
|
113
114
|
children: [
|
|
114
115
|
/*#__PURE__*/ jsx(Dropdown.Trigger, {
|
|
115
116
|
render: /*#__PURE__*/ jsx(IconButton, {
|
|
@@ -187,31 +188,35 @@ function DocumentActions({ publishedVersion, onUnpublish, onDelete, onDuplicate,
|
|
|
187
188
|
})
|
|
188
189
|
})
|
|
189
190
|
}),
|
|
190
|
-
/*#__PURE__*/
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
191
|
+
onDelete && /*#__PURE__*/ jsxs(Fragment, {
|
|
192
|
+
children: [
|
|
193
|
+
/*#__PURE__*/ jsx(Dropdown.Separator, {}),
|
|
194
|
+
/*#__PURE__*/ jsx(Dropdown.Item, {
|
|
195
|
+
onClick: ()=>{
|
|
196
|
+
setShowDeleteConfirm(true);
|
|
197
|
+
},
|
|
198
|
+
children: /*#__PURE__*/ jsxs("div", {
|
|
199
|
+
className: clsx('byline-form-actions-item', document_actions_module.item),
|
|
200
|
+
children: [
|
|
201
|
+
/*#__PURE__*/ jsx("span", {
|
|
202
|
+
className: clsx('byline-form-actions-item-icon', document_actions_module["item-icon"]),
|
|
203
|
+
children: /*#__PURE__*/ jsx(DeleteIcon, {
|
|
204
|
+
width: "16px",
|
|
205
|
+
height: "16px"
|
|
206
|
+
})
|
|
207
|
+
}),
|
|
208
|
+
/*#__PURE__*/ jsx("span", {
|
|
209
|
+
className: clsx('byline-form-actions-item-text', document_actions_module["item-text"]),
|
|
210
|
+
children: /*#__PURE__*/ jsx("button", {
|
|
211
|
+
type: "button",
|
|
212
|
+
className: clsx('byline-form-actions-delete', document_actions_module["delete"]),
|
|
213
|
+
children: t('common.actions.delete')
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
]
|
|
212
217
|
})
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
})
|
|
219
|
+
]
|
|
215
220
|
})
|
|
216
221
|
]
|
|
217
222
|
})
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
import { type ReactNode } from 'react';
|
|
9
|
-
import type {
|
|
9
|
+
import type { Field, FormAdminConfig, WorkflowStatus } from '@byline/core';
|
|
10
10
|
import type { DocumentPatch } from '@byline/core/patches';
|
|
11
11
|
import { type DocumentActionsLocaleOption } from './document-actions';
|
|
12
12
|
import { type ScheduledPublicationInfo, type SchedulePublicationInput } from './scheduled-publication-control';
|
|
@@ -39,7 +39,7 @@ export interface SystemFieldsSubmitPayload {
|
|
|
39
39
|
export interface FormRendererProps {
|
|
40
40
|
mode: 'create' | 'edit';
|
|
41
41
|
fields: Field[];
|
|
42
|
-
onSubmit: (data: any) => void
|
|
42
|
+
onSubmit: (data: any) => void | Promise<void>;
|
|
43
43
|
onCancel: () => void;
|
|
44
44
|
onStatusChange?: (nextStatus: string) => Promise<void>;
|
|
45
45
|
onUnpublish?: () => Promise<void>;
|
|
@@ -86,7 +86,7 @@ export interface FormRendererProps {
|
|
|
86
86
|
workflowStatuses?: WorkflowStatus[];
|
|
87
87
|
publishedVersion?: PublishedVersionInfo | null;
|
|
88
88
|
initialData?: Record<string, any>;
|
|
89
|
-
adminConfig?:
|
|
89
|
+
adminConfig?: FormAdminConfig;
|
|
90
90
|
/**
|
|
91
91
|
* Name of the schema field to render as the live form heading.
|
|
92
92
|
* Sourced from `CollectionDefinition.useAsTitle` by the caller.
|
|
@@ -98,6 +98,20 @@ export interface FormRendererProps {
|
|
|
98
98
|
* present the path widget renders in the sidebar.
|
|
99
99
|
*/
|
|
100
100
|
useAsPath?: string;
|
|
101
|
+
/**
|
|
102
|
+
* Whether the system path widget may render in the sidebar. Defaults to
|
|
103
|
+
* `true`, which preserves the historical behaviour of showing the widget
|
|
104
|
+
* whenever `useAsPath` is declared or the document envelope carries a
|
|
105
|
+
* `path`. Set `false` for a resource whose path is internal metadata and
|
|
106
|
+
* must never be presented or edited.
|
|
107
|
+
*/
|
|
108
|
+
showPath?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Explicit form heading, used verbatim. Overrides both `useAsTitle`'s live
|
|
111
|
+
* value and the create/edit wording derived from `mode` — for a resource
|
|
112
|
+
* whose identity does not change when it is first materialised.
|
|
113
|
+
*/
|
|
114
|
+
heading?: string;
|
|
101
115
|
/**
|
|
102
116
|
* Opts the available-locales widget into the sidebar (below the path
|
|
103
117
|
* widget). Sourced from `CollectionDefinition.advertiseLocales` by the
|
|
@@ -24,13 +24,15 @@ import { computeStatusTransitions } from "./status-transitions.js";
|
|
|
24
24
|
import { TreePlacementWidget } from "./tree-placement-widget.js";
|
|
25
25
|
import { executeUploadsWithProgress } from "./upload-executor.js";
|
|
26
26
|
import { useFormLayout } from "./use-form-layout.js";
|
|
27
|
-
const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, scheduledPublication, onSchedulePublication, onConfirmScheduledPublication, onCancelScheduledPublication, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, tree, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale = 'en', useNavigationGuard: useNavigationGuardProp, restoreWarnings, _activeTabBySet, _onTabChange })=>{
|
|
27
|
+
const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, scheduledPublication, onSchedulePublication, onConfirmScheduledPublication, onCancelScheduledPublication, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, showPath = true, heading, advertiseLocales, tree, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale = 'en', useNavigationGuard: useNavigationGuardProp, restoreWarnings, _activeTabBySet, _onTabChange })=>{
|
|
28
28
|
const { getFieldValues, runFieldHooks, validateForm, errors: initialErrors, hasChanges: hasChangesFn, resetHasChanges, getPatches, getDirtyBreakdown, getSystemPath, getSystemAvailableLocales, subscribeErrors, subscribeMeta, setFieldValue, setFieldError, getPendingUploads, clearPendingUploads, setFieldUploading } = useFormContext();
|
|
29
29
|
const { t } = useTranslation('byline-admin');
|
|
30
30
|
const [errors, setErrors] = useState(initialErrors);
|
|
31
31
|
const [hasChanges, setHasChanges] = useState(hasChangesFn());
|
|
32
32
|
const [statusBusy, setStatusBusy] = useState(false);
|
|
33
33
|
const [isUploading, setIsUploading] = useState(false);
|
|
34
|
+
const submittingRef = useRef(false);
|
|
35
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
34
36
|
const [showUnsavedModal, setShowUnsavedModal] = useState(false);
|
|
35
37
|
const [pendingSystemFieldsSubmit, setPendingSystemFieldsSubmit] = useState(null);
|
|
36
38
|
const [contentLocale, setContentLocale] = useState(initialLocale ?? defaultLocale);
|
|
@@ -83,7 +85,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
83
85
|
]);
|
|
84
86
|
const [formData, setFormData] = useState(()=>getFieldValues());
|
|
85
87
|
const liveTitle = useFieldValue(useAsTitle ?? '');
|
|
86
|
-
const
|
|
88
|
+
const computedHeading = heading || liveTitle || (headingLabel ? 'create' === mode ? t('forms.heading.createLabel', {
|
|
87
89
|
label: headingLabel
|
|
88
90
|
}) : t('forms.heading.editLabel', {
|
|
89
91
|
label: headingLabel
|
|
@@ -107,10 +109,17 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
107
109
|
const handleCancel = ()=>{
|
|
108
110
|
if (onCancel && 'function' == typeof onCancel) onCancel();
|
|
109
111
|
};
|
|
110
|
-
const submitPayload = useCallback((payload)=>{
|
|
111
|
-
if (
|
|
112
|
-
|
|
112
|
+
const submitPayload = useCallback(async (payload)=>{
|
|
113
|
+
if ('function' != typeof onSubmit) return;
|
|
114
|
+
if (submittingRef.current) return;
|
|
115
|
+
submittingRef.current = true;
|
|
116
|
+
setIsSubmitting(true);
|
|
117
|
+
try {
|
|
118
|
+
await onSubmit(payload);
|
|
113
119
|
resetHasChanges();
|
|
120
|
+
} catch {} finally{
|
|
121
|
+
submittingRef.current = false;
|
|
122
|
+
setIsSubmitting(false);
|
|
114
123
|
}
|
|
115
124
|
}, [
|
|
116
125
|
onSubmit,
|
|
@@ -169,7 +178,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
169
178
|
systemAvailableLocales
|
|
170
179
|
};
|
|
171
180
|
if ('edit' === mode && ('direct-write' === reason || 'both' === reason)) return void setPendingSystemFieldsSubmit(payload);
|
|
172
|
-
submitPayload(payload);
|
|
181
|
+
await submitPayload(payload);
|
|
173
182
|
})();
|
|
174
183
|
};
|
|
175
184
|
const tabErrorCountsBySet = useMemo(()=>{
|
|
@@ -249,7 +258,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
249
258
|
children: [
|
|
250
259
|
/*#__PURE__*/ jsx("h1", {
|
|
251
260
|
className: clsx('byline-form-heading', form_renderer_module.heading),
|
|
252
|
-
children:
|
|
261
|
+
children: computedHeading
|
|
253
262
|
}),
|
|
254
263
|
headerSlot
|
|
255
264
|
]
|
|
@@ -285,7 +294,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
285
294
|
className: clsx('byline-form-actions-button', form_renderer_module["actions-button"]),
|
|
286
295
|
size: "sm",
|
|
287
296
|
type: "submit",
|
|
288
|
-
disabled: false === hasChanges || isUploading,
|
|
297
|
+
disabled: false === hasChanges || isUploading || isSubmitting,
|
|
289
298
|
children: isUploading ? t('forms.actions.uploading') : t('common.actions.save')
|
|
290
299
|
}),
|
|
291
300
|
primaryStatus && onStatusChange && /*#__PURE__*/ jsx("div", {
|
|
@@ -386,7 +395,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
386
395
|
/*#__PURE__*/ jsxs("div", {
|
|
387
396
|
className: clsx('byline-form-sidebar', form_renderer_module.sidebar),
|
|
388
397
|
children: [
|
|
389
|
-
(useAsPath || 'string' == typeof initialData?.path && initialData.path.length > 0) && /*#__PURE__*/ jsx(PathWidget, {
|
|
398
|
+
showPath && (useAsPath || 'string' == typeof initialData?.path && initialData.path.length > 0) && /*#__PURE__*/ jsx(PathWidget, {
|
|
390
399
|
useAsPath: useAsPath,
|
|
391
400
|
collectionPath: collectionPath ?? '',
|
|
392
401
|
defaultLocale: defaultLocale,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
|
-
import { getCollectionDefinition } from "@byline/core";
|
|
4
|
+
import { getCollectionDefinition, isSingleton } from "@byline/core";
|
|
5
5
|
import { useTranslation } from "@byline/i18n/react";
|
|
6
6
|
import { Button } from "@byline/ui/react";
|
|
7
7
|
import clsx from "clsx";
|
|
@@ -11,7 +11,8 @@ import tree_placement_widget_module from "./tree-placement-widget.module.js";
|
|
|
11
11
|
const TreePlacementWidget = ({ collectionPath, documentId, useAsTitle })=>{
|
|
12
12
|
const { t } = useTranslation('byline-admin');
|
|
13
13
|
const { getTreeAncestors, getTreeParent, placeTreeNode } = useBylineFieldServices();
|
|
14
|
-
const
|
|
14
|
+
const definition = getCollectionDefinition(collectionPath);
|
|
15
|
+
const targetDefinition = null == definition || isSingleton(definition) ? null : definition;
|
|
15
16
|
const [parent, setParent] = useState(null);
|
|
16
17
|
const [placed, setPlaced] = useState(true);
|
|
17
18
|
const [loading, setLoading] = useState(true);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Field, FormAdminConfig, GroupDefinition, LayoutDefinition, RowDefinition, TabSetDefinition } from '@byline/core';
|
|
2
2
|
/** Which tab set + tab a schema field is placed under. */
|
|
3
3
|
export interface TabPath {
|
|
4
4
|
tabSetName: string;
|
|
@@ -27,9 +27,9 @@ export interface FormLayout {
|
|
|
27
27
|
* `layout.main`) are absent from the map. Rows and groups are recursed into;
|
|
28
28
|
* the `seen` set guards against a config that references a row/group cycle.
|
|
29
29
|
*/
|
|
30
|
-
export declare function buildFieldToTabPath(adminConfig:
|
|
30
|
+
export declare function buildFieldToTabPath(adminConfig: FormAdminConfig | undefined, fieldByName: Map<string, Field>, rowByName: Map<string, RowDefinition>, groupByName: Map<string, GroupDefinition>): Map<string, TabPath>;
|
|
31
31
|
/**
|
|
32
32
|
* Memoise the layout primitives + lookup tables the form renderer walks.
|
|
33
33
|
* Rebuilt only when `adminConfig` / `fields` change.
|
|
34
34
|
*/
|
|
35
|
-
export declare function useFormLayout(adminConfig:
|
|
35
|
+
export declare function useFormLayout(adminConfig: FormAdminConfig | undefined, fields: Field[]): FormLayout;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/admin",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.17.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -179,34 +179,32 @@
|
|
|
179
179
|
"@lezer/highlight": "^1.2.3",
|
|
180
180
|
"@tanstack/react-form-start": "^1.33.5",
|
|
181
181
|
"clsx": "^2.1.1",
|
|
182
|
-
"jose": "^6.2.
|
|
182
|
+
"jose": "^6.2.10",
|
|
183
183
|
"react-diff-viewer-continued": "^4.4.0",
|
|
184
184
|
"uuid": "^14.0.2",
|
|
185
185
|
"zod": "^4.4.3",
|
|
186
|
-
"@byline/
|
|
187
|
-
"@byline/i18n": "4.
|
|
188
|
-
"@byline/
|
|
189
|
-
"@byline/
|
|
190
|
-
"@byline/
|
|
191
|
-
"@byline/
|
|
186
|
+
"@byline/auth": "4.17.0",
|
|
187
|
+
"@byline/i18n": "4.17.0",
|
|
188
|
+
"@byline/analytics": "4.17.0",
|
|
189
|
+
"@byline/analytics-agent": "4.17.0",
|
|
190
|
+
"@byline/core": "4.17.0",
|
|
191
|
+
"@byline/ui": "4.17.0"
|
|
192
192
|
},
|
|
193
193
|
"peerDependencies": {
|
|
194
194
|
"react": "^19.0.0",
|
|
195
195
|
"react-dom": "^19.0.0"
|
|
196
196
|
},
|
|
197
197
|
"devDependencies": {
|
|
198
|
-
"@biomejs/biome": "2.5.
|
|
198
|
+
"@biomejs/biome": "2.5.10",
|
|
199
199
|
"@rsbuild/plugin-react": "^2.1.0",
|
|
200
200
|
"@rslib/core": "^0.23.2",
|
|
201
|
-
"@types/node": "^26.
|
|
201
|
+
"@types/node": "^26.4.0",
|
|
202
202
|
"@types/react": "19.2.18",
|
|
203
|
-
"@types/react-dom": "19.2.
|
|
203
|
+
"@types/react-dom": "19.2.5",
|
|
204
204
|
"@vitejs/plugin-react": "^6.1.0",
|
|
205
205
|
"jsdom": "^30.0.1",
|
|
206
206
|
"react": "^19.2.8",
|
|
207
207
|
"react-dom": "^19.2.8",
|
|
208
|
-
"rimraf": "^6.1.3",
|
|
209
|
-
"tsx": "^4.23.12",
|
|
210
208
|
"typescript": "^7.0.2",
|
|
211
209
|
"typescript-plugin-css-modules": "^5.2.0",
|
|
212
210
|
"vitest": "^4.1.11"
|
|
@@ -10,11 +10,11 @@ import { useState } from 'react'
|
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
12
|
CollectionAdminConfig,
|
|
13
|
-
CollectionDefinition,
|
|
14
13
|
RelationField as FieldType,
|
|
14
|
+
MultiCollectionDefinition,
|
|
15
15
|
RelatedDocumentValue,
|
|
16
16
|
} from '@byline/core'
|
|
17
|
-
import { getCollectionAdminConfig, getCollectionDefinition } from '@byline/core'
|
|
17
|
+
import { getCollectionAdminConfig, getCollectionDefinition, isSingleton } from '@byline/core'
|
|
18
18
|
import { useTranslation } from '@byline/i18n/react'
|
|
19
19
|
import {
|
|
20
20
|
Button,
|
|
@@ -80,7 +80,9 @@ export const RelationField = ({
|
|
|
80
80
|
// config drives the picker-column rendering inside RelationSummary so
|
|
81
81
|
// the selected tile matches the picker row exactly. Missing target →
|
|
82
82
|
// render an inline error and disable the picker.
|
|
83
|
-
const
|
|
83
|
+
const targetDefinition = getCollectionDefinition(field.targetCollection)
|
|
84
|
+
const targetDef: MultiCollectionDefinition | null =
|
|
85
|
+
targetDefinition != null && !isSingleton(targetDefinition) ? targetDefinition : null
|
|
84
86
|
const targetAdminConfig: CollectionAdminConfig | null = getCollectionAdminConfig(
|
|
85
87
|
field.targetCollection
|
|
86
88
|
)
|
|
@@ -10,11 +10,11 @@ import { type ReactNode, useState } from 'react'
|
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
12
|
CollectionAdminConfig,
|
|
13
|
-
CollectionDefinition,
|
|
14
13
|
RelationField as FieldType,
|
|
14
|
+
MultiCollectionDefinition,
|
|
15
15
|
RelatedDocumentValue,
|
|
16
16
|
} from '@byline/core'
|
|
17
|
-
import { getCollectionAdminConfig, getCollectionDefinition } from '@byline/core'
|
|
17
|
+
import { getCollectionAdminConfig, getCollectionDefinition, isSingleton } from '@byline/core'
|
|
18
18
|
import { useTranslation } from '@byline/i18n/react'
|
|
19
19
|
import {
|
|
20
20
|
Button,
|
|
@@ -122,7 +122,9 @@ export const RelationManyField = ({ field, defaultValue, id, path }: RelationMan
|
|
|
122
122
|
const fieldError = useFieldError(fieldPath)
|
|
123
123
|
const { getFieldValue, setFieldValue } = useFormContext()
|
|
124
124
|
|
|
125
|
-
const
|
|
125
|
+
const targetDefinition = getCollectionDefinition(field.targetCollection)
|
|
126
|
+
const targetDef: MultiCollectionDefinition | null =
|
|
127
|
+
targetDefinition != null && !isSingleton(targetDefinition) ? targetDefinition : null
|
|
126
128
|
const targetAdminConfig: CollectionAdminConfig | null = getCollectionAdminConfig(
|
|
127
129
|
field.targetCollection
|
|
128
130
|
)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { useCallback, useEffect, useState } from 'react'
|
|
10
10
|
|
|
11
|
-
import type { CollectionAdminConfig,
|
|
11
|
+
import type { CollectionAdminConfig, MultiCollectionDefinition } from '@byline/core'
|
|
12
12
|
import { getCollectionAdminConfig } from '@byline/core'
|
|
13
13
|
import { useTranslation } from '@byline/i18n/react'
|
|
14
14
|
import { Button, CheckIcon, LoaderRing, Modal, Search } from '@byline/ui/react'
|
|
@@ -57,7 +57,7 @@ interface RelationPickerBaseProps {
|
|
|
57
57
|
/** The target collection path (e.g. `'media'`). */
|
|
58
58
|
targetCollectionPath: string
|
|
59
59
|
/** The target collection definition (used for labels + displayField fallback). */
|
|
60
|
-
targetDefinition?:
|
|
60
|
+
targetDefinition?: MultiCollectionDefinition | null
|
|
61
61
|
/** Explicit display field to render as row label. */
|
|
62
62
|
displayField?: string
|
|
63
63
|
/**
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Copyright (c) Infonomic Company Limited
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { CollectionAdminConfig,
|
|
9
|
+
import type { CollectionAdminConfig, MultiCollectionDefinition } from '@byline/core'
|
|
10
10
|
import cx from 'clsx'
|
|
11
11
|
|
|
12
12
|
import { PickerCell, resolveFallbackDisplayField, resolveRowLabel } from './relation-display'
|
|
@@ -34,7 +34,7 @@ import styles from './relation-summary.module.css'
|
|
|
34
34
|
// ---------------------------------------------------------------------------
|
|
35
35
|
|
|
36
36
|
interface RelationSummaryProps {
|
|
37
|
-
targetDefinition:
|
|
37
|
+
targetDefinition: MultiCollectionDefinition
|
|
38
38
|
targetAdminConfig: CollectionAdminConfig | null
|
|
39
39
|
displayField?: string
|
|
40
40
|
/** The raw relation value from the form. May be a plain ref or a populated envelope. */
|
|
@@ -0,0 +1,157 @@
|
|
|
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 { act } from 'react'
|
|
10
|
+
|
|
11
|
+
import { defineAdminConfig } from '@byline/core'
|
|
12
|
+
import { adminTranslations } from '@byline/i18n/admin'
|
|
13
|
+
import { I18nProvider } from '@byline/i18n/react'
|
|
14
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
16
|
+
|
|
17
|
+
import { BylineFieldServicesProvider } from '../fields/field-services-context'
|
|
18
|
+
|
|
19
|
+
// Spread the REAL uikit and override only the three primitives that make
|
|
20
|
+
// assertions hard. A factory mock replaces the whole module, so anything not
|
|
21
|
+
// returned becomes `undefined` and React throws "Element type is invalid" on
|
|
22
|
+
// mount — FormRenderer alone pulls in Alert, Button, and ComboButton
|
|
23
|
+
// (`form-renderer.tsx:24`), and its field/presentation subtree pulls in more.
|
|
24
|
+
// `@byline/ui` ships a built `dist` plus a `development` source condition, so
|
|
25
|
+
// `importOriginal()` resolves without a prior build step.
|
|
26
|
+
vi.mock('@byline/ui/react', async (importOriginal) => {
|
|
27
|
+
const actual = await importOriginal<Record<string, unknown>>()
|
|
28
|
+
const Pass = ({ children }: any) => <div>{children}</div>
|
|
29
|
+
const Modal: any = ({ children, isOpen }: any) => (isOpen ? <div>{children}</div> : null)
|
|
30
|
+
Modal.Container = Pass
|
|
31
|
+
Modal.Header = Pass
|
|
32
|
+
Modal.Content = Pass
|
|
33
|
+
Modal.Actions = Pass
|
|
34
|
+
return {
|
|
35
|
+
...actual,
|
|
36
|
+
Dropdown: {
|
|
37
|
+
Root: Pass,
|
|
38
|
+
Trigger: ({ children }: any) => <div data-testid="actions-trigger">{children}</div>,
|
|
39
|
+
Portal: Pass,
|
|
40
|
+
Content: Pass,
|
|
41
|
+
Item: ({ children, onClick }: any) => (
|
|
42
|
+
<div
|
|
43
|
+
onClick={onClick}
|
|
44
|
+
onKeyDown={(event) => {
|
|
45
|
+
if (event.key === 'Enter' || event.key === ' ') onClick?.(event)
|
|
46
|
+
}}
|
|
47
|
+
role="menuitem"
|
|
48
|
+
tabIndex={0}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</div>
|
|
52
|
+
),
|
|
53
|
+
Separator: () => <hr />,
|
|
54
|
+
},
|
|
55
|
+
Modal,
|
|
56
|
+
// Overridden so `input[name=...]` is a stable selector for typing into
|
|
57
|
+
// fields; every other uikit export comes through from `actual`.
|
|
58
|
+
Input: ({ name, value, onChange }: any) => (
|
|
59
|
+
<input name={name} value={value ?? ''} onChange={onChange} />
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Silences React's "not configured to support act(...)" warning. Copied from
|
|
65
|
+
// `path-widget.test.tsx:76`.
|
|
66
|
+
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
|
67
|
+
|
|
68
|
+
// FormRenderer calls `getAdminConfig()` unconditionally (`form-renderer.tsx:279`)
|
|
69
|
+
// to resolve the path widget's slugifier. The *use* of the result is gated on
|
|
70
|
+
// `useAsPath`, but the call is not — with no config registered it throws
|
|
71
|
+
// "Byline has not been configured yet" and the mount fails. `AdminConfig`
|
|
72
|
+
// requires only `i18n` and `collections`, so register a real one rather than
|
|
73
|
+
// mocking `@byline/core`; it lives on a global, so module scope runs it once.
|
|
74
|
+
defineAdminConfig({
|
|
75
|
+
i18n: {
|
|
76
|
+
admin: { defaultLocale: 'en', locales: ['en'] },
|
|
77
|
+
content: { defaultLocale: 'en', locales: ['en'] },
|
|
78
|
+
},
|
|
79
|
+
collections: [
|
|
80
|
+
{
|
|
81
|
+
path: 'pages',
|
|
82
|
+
labels: { singular: 'Page', plural: 'Pages' },
|
|
83
|
+
fields: [{ name: 'title', label: 'Title', type: 'text' }],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
slugifier: (value: string) => value.toLowerCase().trim().replace(/\s+/g, '-'),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// FormRenderer also calls `useBylineFieldServices()` unconditionally
|
|
90
|
+
// (`form-renderer.tsx:271`), which throws when the provider is absent. Only
|
|
91
|
+
// `getCollectionDocuments` and `uploadField` are required members; the tree
|
|
92
|
+
// functions are optional. Cast the stub — these tests never invoke it, and
|
|
93
|
+
// pinning the real signatures here would couple them to unrelated drift.
|
|
94
|
+
const fieldServices = {
|
|
95
|
+
getCollectionDocuments: async () => ({ docs: [], total: 0 }),
|
|
96
|
+
uploadField: async () => ({}),
|
|
97
|
+
} as any
|
|
98
|
+
|
|
99
|
+
let container: HTMLDivElement
|
|
100
|
+
let root: Root
|
|
101
|
+
|
|
102
|
+
beforeEach(() => {
|
|
103
|
+
container = document.createElement('div')
|
|
104
|
+
document.body.appendChild(container)
|
|
105
|
+
root = createRoot(container)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
act(() => {
|
|
110
|
+
root.unmount()
|
|
111
|
+
})
|
|
112
|
+
container.remove()
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const renderInProvider = (element: React.ReactNode) => {
|
|
116
|
+
act(() => {
|
|
117
|
+
root.render(
|
|
118
|
+
<I18nProvider
|
|
119
|
+
bundle={adminTranslations({ locales: ['en'] })}
|
|
120
|
+
activeLocale="en"
|
|
121
|
+
defaultLocale="en"
|
|
122
|
+
localeDefinitions={[{ code: 'en', nativeName: 'English' }]}
|
|
123
|
+
>
|
|
124
|
+
<BylineFieldServicesProvider services={fieldServices}>
|
|
125
|
+
{element}
|
|
126
|
+
</BylineFieldServicesProvider>
|
|
127
|
+
</I18nProvider>
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
import { DocumentActions } from './document-actions'
|
|
133
|
+
|
|
134
|
+
describe('DocumentActions', () => {
|
|
135
|
+
const render = (props: Record<string, unknown>) =>
|
|
136
|
+
renderInProvider(<DocumentActions {...(props as any)} />)
|
|
137
|
+
|
|
138
|
+
it('hides the Delete item when no onDelete handler is supplied', () => {
|
|
139
|
+
render({})
|
|
140
|
+
expect(container.textContent).not.toContain('Delete')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('renders the Delete item when an onDelete handler is supplied', () => {
|
|
144
|
+
render({ onDelete: async () => {} })
|
|
145
|
+
expect(container.textContent).toContain('Delete')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('renders no trigger at all when no actions are available', () => {
|
|
149
|
+
render({})
|
|
150
|
+
expect(container.querySelector('[data-testid="actions-trigger"]')).toBeNull()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('renders the trigger when at least one action is available', () => {
|
|
154
|
+
render({ onDuplicate: async () => {} })
|
|
155
|
+
expect(container.querySelector('[data-testid="actions-trigger"]')).not.toBeNull()
|
|
156
|
+
})
|
|
157
|
+
})
|