@byline/admin 3.12.2 → 3.13.1
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/field-services-types.d.ts +50 -0
- package/dist/forms/form-renderer.d.ts +8 -1
- package/dist/forms/form-renderer.js +9 -2
- package/dist/forms/tree-placement-widget.d.ts +23 -0
- package/dist/forms/tree-placement-widget.js +176 -0
- package/dist/forms/tree-placement-widget.module.js +15 -0
- package/dist/forms/tree-placement-widget_module.css +69 -0
- package/dist/react.d.ts +1 -1
- package/package.json +5 -5
- package/src/fields/field-services-types.ts +50 -0
- package/src/forms/form-renderer.tsx +18 -0
- package/src/forms/tree-placement-widget.module.css +87 -0
- package/src/forms/tree-placement-widget.tsx +202 -0
- package/src/react.ts +6 -0
|
@@ -57,7 +57,57 @@ export interface UploadedFileResult {
|
|
|
57
57
|
storedFile: StoredFileValue;
|
|
58
58
|
}
|
|
59
59
|
export type UploadFieldFn = (collection: string, formData: FormData, createDocument?: boolean) => Promise<UploadedFileResult>;
|
|
60
|
+
/** One hydrated ancestor in a document's breadcrumb trail (root-first). */
|
|
61
|
+
export interface TreeAncestor {
|
|
62
|
+
id: string;
|
|
63
|
+
title: string;
|
|
64
|
+
path?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface PlaceTreeNodeInput {
|
|
67
|
+
collection: string;
|
|
68
|
+
documentId: string;
|
|
69
|
+
/** The new parent; `null` makes the document a root node. */
|
|
70
|
+
parentDocumentId: string | null;
|
|
71
|
+
/** Optional sibling neighbours (left = land after, right = land before). */
|
|
72
|
+
beforeDocumentId?: string | null;
|
|
73
|
+
afterDocumentId?: string | null;
|
|
74
|
+
}
|
|
75
|
+
/** Place / move a document within its collection's tree. */
|
|
76
|
+
export type PlaceTreeNodeFn = (input: PlaceTreeNodeInput) => Promise<{
|
|
77
|
+
orderKey: string;
|
|
78
|
+
}>;
|
|
79
|
+
/** Remove a document from the tree (back to the unplaced state). */
|
|
80
|
+
export type RemoveFromTreeFn = (input: {
|
|
81
|
+
collection: string;
|
|
82
|
+
documentId: string;
|
|
83
|
+
}) => Promise<void>;
|
|
84
|
+
/** Resolve a document's ancestor chain, root-first, hydrated with titles. */
|
|
85
|
+
export type GetTreeAncestorsFn = (input: {
|
|
86
|
+
collection: string;
|
|
87
|
+
documentId: string;
|
|
88
|
+
}) => Promise<TreeAncestor[]>;
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a document's placement state — the tri-state (unplaced / root / child)
|
|
91
|
+
* that `getTreeAncestors` cannot express (it returns `[]` for both root and
|
|
92
|
+
* unplaced). `placed: false` = unplaced; `placed: true` + null parent = root.
|
|
93
|
+
*/
|
|
94
|
+
export type GetTreeParentFn = (input: {
|
|
95
|
+
collection: string;
|
|
96
|
+
documentId: string;
|
|
97
|
+
}) => Promise<{
|
|
98
|
+
placed: boolean;
|
|
99
|
+
parentDocumentId: string | null;
|
|
100
|
+
}>;
|
|
60
101
|
export interface BylineFieldServices {
|
|
61
102
|
getCollectionDocuments: GetCollectionDocumentsFn;
|
|
62
103
|
uploadField: UploadFieldFn;
|
|
104
|
+
/**
|
|
105
|
+
* Document-tree operations, consumed by the sidebar tree-placement widget.
|
|
106
|
+
* Optional — only hosts that serve `tree: true` collections need to wire
|
|
107
|
+
* them; the widget guards on their presence.
|
|
108
|
+
*/
|
|
109
|
+
placeTreeNode?: PlaceTreeNodeFn;
|
|
110
|
+
removeFromTree?: RemoveFromTreeFn;
|
|
111
|
+
getTreeAncestors?: GetTreeAncestorsFn;
|
|
112
|
+
getTreeParent?: GetTreeParentFn;
|
|
63
113
|
}
|
|
@@ -100,6 +100,13 @@ export interface FormRendererProps {
|
|
|
100
100
|
* against the document's `_availableVersionLocales` ledger fact.
|
|
101
101
|
*/
|
|
102
102
|
advertiseLocales?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Opts the document-tree placement widget into the sidebar (above the
|
|
105
|
+
* available-locales widget). Sourced from `CollectionDefinition.tree` by the
|
|
106
|
+
* caller. Renders only in edit mode (placement needs a persisted document)
|
|
107
|
+
* and only when the host wires the tree services. See docs/DOCUMENT-TREE.md.
|
|
108
|
+
*/
|
|
109
|
+
tree?: boolean;
|
|
103
110
|
headingLabel?: string;
|
|
104
111
|
headerSlot?: ReactNode;
|
|
105
112
|
/** Collection path forwarded to upload-capable fields (e.g. `'media'`). */
|
|
@@ -129,4 +136,4 @@ export interface FormRendererProps {
|
|
|
129
136
|
*/
|
|
130
137
|
useNavigationGuard?: UseNavigationGuard;
|
|
131
138
|
}
|
|
132
|
-
export declare const FormRenderer: ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings, }: FormRendererProps) => import("react").JSX.Element;
|
|
139
|
+
export declare const FormRenderer: ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, tree, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings, }: FormRendererProps) => import("react").JSX.Element;
|
|
@@ -18,9 +18,10 @@ import { FormStatusDisplay } from "./form-status-display.js";
|
|
|
18
18
|
import { useNavigationGuardAdapter } from "./navigation-guard.js";
|
|
19
19
|
import { PathWidget } from "./path-widget.js";
|
|
20
20
|
import { computeStatusTransitions } from "./status-transitions.js";
|
|
21
|
+
import { TreePlacementWidget } from "./tree-placement-widget.js";
|
|
21
22
|
import { executeUploadsWithProgress } from "./upload-executor.js";
|
|
22
23
|
import { useFormLayout } from "./use-form-layout.js";
|
|
23
|
-
const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale = 'en', useNavigationGuard: useNavigationGuardProp, restoreWarnings, _activeTabBySet, _onTabChange })=>{
|
|
24
|
+
const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, 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 })=>{
|
|
24
25
|
const { getFieldValues, runFieldHooks, validateForm, errors: initialErrors, hasChanges: hasChangesFn, resetHasChanges, getPatches, getDirtyBreakdown, getSystemPath, getSystemAvailableLocales, subscribeErrors, subscribeMeta, setFieldValue, setFieldError, getPendingUploads, clearPendingUploads, setFieldUploading } = useFormContext();
|
|
25
26
|
const { t } = useTranslation('byline-admin');
|
|
26
27
|
const [errors, setErrors] = useState(initialErrors);
|
|
@@ -346,6 +347,11 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
346
347
|
activeLocale: contentLocale,
|
|
347
348
|
mode: mode
|
|
348
349
|
}),
|
|
350
|
+
tree && 'edit' === mode && 'string' == typeof initialData?.id && /*#__PURE__*/ jsx(TreePlacementWidget, {
|
|
351
|
+
collectionPath: collectionPath ?? '',
|
|
352
|
+
documentId: initialData.id,
|
|
353
|
+
useAsTitle: useAsTitle
|
|
354
|
+
}),
|
|
349
355
|
advertiseLocales && /*#__PURE__*/ jsx(AvailableLocalesWidget, {
|
|
350
356
|
contentLocales: contentLocales ?? [],
|
|
351
357
|
availableVersionLocales: initialData?._availableVersionLocales ?? []
|
|
@@ -376,7 +382,7 @@ const FormContent = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpub
|
|
|
376
382
|
]
|
|
377
383
|
});
|
|
378
384
|
};
|
|
379
|
-
const FormRenderer = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings })=>{
|
|
385
|
+
const FormRenderer = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpublish, onDelete, onDuplicate, onCopyToLocale, onDeleteLocale, contentLocales, nextStatus, workflowStatuses, publishedVersion, initialData, adminConfig, useAsTitle, useAsPath, advertiseLocales, tree, headingLabel, headerSlot, collectionPath, initialLocale, onLocaleChange, defaultLocale, useNavigationGuard, restoreWarnings })=>{
|
|
380
386
|
const savedTabsRef = useRef({});
|
|
381
387
|
return /*#__PURE__*/ jsx(FormProvider, {
|
|
382
388
|
initialData: initialData,
|
|
@@ -400,6 +406,7 @@ const FormRenderer = ({ mode, fields, onSubmit, onCancel, onStatusChange, onUnpu
|
|
|
400
406
|
useAsTitle: useAsTitle,
|
|
401
407
|
useAsPath: useAsPath,
|
|
402
408
|
advertiseLocales: advertiseLocales,
|
|
409
|
+
tree: tree,
|
|
403
410
|
headingLabel: headingLabel,
|
|
404
411
|
headerSlot: headerSlot,
|
|
405
412
|
collectionPath: collectionPath,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface TreePlacementWidgetProps {
|
|
2
|
+
/** The collection path (`tree: true`). */
|
|
3
|
+
collectionPath: string;
|
|
4
|
+
/** The logical id of the document being edited. */
|
|
5
|
+
documentId: string;
|
|
6
|
+
/** The collection's `useAsTitle` field, used to label the chosen parent. */
|
|
7
|
+
useAsTitle?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Sidebar widget for placing the current document within its collection's
|
|
11
|
+
* single-parent document tree (the `tree: true` primitive — docs/DOCUMENT-TREE.md).
|
|
12
|
+
*
|
|
13
|
+
* The tree is document-grain and **unversioned**, so changes here write
|
|
14
|
+
* immediately (independent of the form's content save). The editor picks a
|
|
15
|
+
* parent through the collection's own relation picker — same search / columns
|
|
16
|
+
* UX as a relation field — or moves the document to the top level; the server
|
|
17
|
+
* enforces the cycle / same-collection invariants and fires the
|
|
18
|
+
* structural-change invalidation event.
|
|
19
|
+
*
|
|
20
|
+
* Renders only in edit mode (placement needs a persisted document) and only when
|
|
21
|
+
* the host wires the tree services. Stable override handle: `.byline-form-tree`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const TreePlacementWidget: ({ collectionPath, documentId, useAsTitle, }: TreePlacementWidgetProps) => import("react").JSX.Element | null;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useState } from "react";
|
|
4
|
+
import { getCollectionDefinition } from "@byline/core";
|
|
5
|
+
import { useTranslation } from "@byline/i18n/react";
|
|
6
|
+
import { Button } from "@byline/ui/react";
|
|
7
|
+
import classnames from "classnames";
|
|
8
|
+
import { useBylineFieldServices } from "../fields/field-services-context.js";
|
|
9
|
+
import { RelationPicker } from "../fields/relation/relation-picker.js";
|
|
10
|
+
import tree_placement_widget_module from "./tree-placement-widget.module.js";
|
|
11
|
+
const TreePlacementWidget = ({ collectionPath, documentId, useAsTitle })=>{
|
|
12
|
+
const { t } = useTranslation('byline-admin');
|
|
13
|
+
const { getTreeAncestors, getTreeParent, placeTreeNode } = useBylineFieldServices();
|
|
14
|
+
const targetDefinition = getCollectionDefinition(collectionPath);
|
|
15
|
+
const [parent, setParent] = useState(null);
|
|
16
|
+
const [placed, setPlaced] = useState(true);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [busy, setBusy] = useState(false);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
const [pickerOpen, setPickerOpen] = useState(false);
|
|
21
|
+
const treeServicesReady = null != getTreeAncestors && null != placeTreeNode;
|
|
22
|
+
useEffect(()=>{
|
|
23
|
+
if (null == getTreeAncestors) return;
|
|
24
|
+
let cancelled = false;
|
|
25
|
+
setLoading(true);
|
|
26
|
+
Promise.all([
|
|
27
|
+
getTreeAncestors({
|
|
28
|
+
collection: collectionPath,
|
|
29
|
+
documentId
|
|
30
|
+
}),
|
|
31
|
+
getTreeParent?.({
|
|
32
|
+
collection: collectionPath,
|
|
33
|
+
documentId
|
|
34
|
+
}) ?? Promise.resolve({
|
|
35
|
+
placed: true,
|
|
36
|
+
parentDocumentId: null
|
|
37
|
+
})
|
|
38
|
+
]).then(([ancestors, placement])=>{
|
|
39
|
+
if (cancelled) return;
|
|
40
|
+
const immediate = ancestors.at(-1);
|
|
41
|
+
setParent(immediate ? {
|
|
42
|
+
id: immediate.id,
|
|
43
|
+
title: immediate.title
|
|
44
|
+
} : null);
|
|
45
|
+
setPlaced(placement.placed);
|
|
46
|
+
}).catch(()=>{
|
|
47
|
+
if (!cancelled) setError(t('treeWidget.error'));
|
|
48
|
+
}).finally(()=>{
|
|
49
|
+
if (!cancelled) setLoading(false);
|
|
50
|
+
});
|
|
51
|
+
return ()=>{
|
|
52
|
+
cancelled = true;
|
|
53
|
+
};
|
|
54
|
+
}, [
|
|
55
|
+
getTreeAncestors,
|
|
56
|
+
getTreeParent,
|
|
57
|
+
collectionPath,
|
|
58
|
+
documentId,
|
|
59
|
+
t
|
|
60
|
+
]);
|
|
61
|
+
const place = useCallback(async (parentDocumentId, optimistic)=>{
|
|
62
|
+
if (null == placeTreeNode || busy) return;
|
|
63
|
+
const previousParent = parent;
|
|
64
|
+
const previousPlaced = placed;
|
|
65
|
+
setError(null);
|
|
66
|
+
setBusy(true);
|
|
67
|
+
setParent(optimistic);
|
|
68
|
+
setPlaced(true);
|
|
69
|
+
try {
|
|
70
|
+
await placeTreeNode({
|
|
71
|
+
collection: collectionPath,
|
|
72
|
+
documentId,
|
|
73
|
+
parentDocumentId
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
setParent(previousParent);
|
|
77
|
+
setPlaced(previousPlaced);
|
|
78
|
+
setError(t('treeWidget.error'));
|
|
79
|
+
} finally{
|
|
80
|
+
setBusy(false);
|
|
81
|
+
}
|
|
82
|
+
}, [
|
|
83
|
+
placeTreeNode,
|
|
84
|
+
busy,
|
|
85
|
+
parent,
|
|
86
|
+
placed,
|
|
87
|
+
collectionPath,
|
|
88
|
+
documentId,
|
|
89
|
+
t
|
|
90
|
+
]);
|
|
91
|
+
const handlePick = useCallback((selection)=>{
|
|
92
|
+
setPickerOpen(false);
|
|
93
|
+
const title = useAsTitle ? selection.record?.[useAsTitle] : void 0;
|
|
94
|
+
place(selection.targetDocumentId, {
|
|
95
|
+
id: selection.targetDocumentId,
|
|
96
|
+
title: 'string' == typeof title && title.length > 0 ? title : selection.targetDocumentId
|
|
97
|
+
});
|
|
98
|
+
}, [
|
|
99
|
+
place,
|
|
100
|
+
useAsTitle
|
|
101
|
+
]);
|
|
102
|
+
if (!treeServicesReady) return null;
|
|
103
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
104
|
+
className: classnames('byline-form-tree', tree_placement_widget_module.tree),
|
|
105
|
+
children: [
|
|
106
|
+
/*#__PURE__*/ jsx("span", {
|
|
107
|
+
className: classnames('byline-form-tree-heading', tree_placement_widget_module.heading),
|
|
108
|
+
children: t('treeWidget.label')
|
|
109
|
+
}),
|
|
110
|
+
/*#__PURE__*/ jsxs("div", {
|
|
111
|
+
className: classnames('byline-form-tree-current', tree_placement_widget_module.current),
|
|
112
|
+
children: [
|
|
113
|
+
/*#__PURE__*/ jsx("span", {
|
|
114
|
+
className: tree_placement_widget_module.parentLabel,
|
|
115
|
+
children: t('treeWidget.parentPrefix')
|
|
116
|
+
}),
|
|
117
|
+
' ',
|
|
118
|
+
placed ? parent ? /*#__PURE__*/ jsx("span", {
|
|
119
|
+
className: tree_placement_widget_module.parentValue,
|
|
120
|
+
children: parent.title
|
|
121
|
+
}) : /*#__PURE__*/ jsx("span", {
|
|
122
|
+
className: classnames(tree_placement_widget_module.parentValue, tree_placement_widget_module.root),
|
|
123
|
+
children: t('treeWidget.rootOption')
|
|
124
|
+
}) : /*#__PURE__*/ jsx("span", {
|
|
125
|
+
className: classnames(tree_placement_widget_module.parentValue, tree_placement_widget_module.root),
|
|
126
|
+
children: t('treeWidget.notInTree')
|
|
127
|
+
})
|
|
128
|
+
]
|
|
129
|
+
}),
|
|
130
|
+
/*#__PURE__*/ jsxs("div", {
|
|
131
|
+
className: classnames('byline-form-tree-actions', tree_placement_widget_module.actions),
|
|
132
|
+
children: [
|
|
133
|
+
/*#__PURE__*/ jsx(Button, {
|
|
134
|
+
type: "button",
|
|
135
|
+
size: "xs",
|
|
136
|
+
variant: "outlined",
|
|
137
|
+
intent: "noeffect",
|
|
138
|
+
disabled: loading || busy,
|
|
139
|
+
onClick: ()=>setPickerOpen(true),
|
|
140
|
+
children: t('treeWidget.choose')
|
|
141
|
+
}),
|
|
142
|
+
placed ? null != parent && /*#__PURE__*/ jsx("button", {
|
|
143
|
+
type: "button",
|
|
144
|
+
className: classnames('byline-form-tree-link', tree_placement_widget_module.link),
|
|
145
|
+
disabled: busy,
|
|
146
|
+
onClick: ()=>place(null, null),
|
|
147
|
+
children: t('treeWidget.makeRoot')
|
|
148
|
+
}) : /*#__PURE__*/ jsx("button", {
|
|
149
|
+
type: "button",
|
|
150
|
+
className: classnames('byline-form-tree-link', tree_placement_widget_module.link),
|
|
151
|
+
disabled: loading || busy,
|
|
152
|
+
onClick: ()=>place(null, null),
|
|
153
|
+
children: t('treeWidget.addToTree')
|
|
154
|
+
})
|
|
155
|
+
]
|
|
156
|
+
}),
|
|
157
|
+
null != error && /*#__PURE__*/ jsx("p", {
|
|
158
|
+
className: classnames('byline-form-tree-error', tree_placement_widget_module.error),
|
|
159
|
+
children: error
|
|
160
|
+
}),
|
|
161
|
+
null != targetDefinition && /*#__PURE__*/ jsx(RelationPicker, {
|
|
162
|
+
targetCollectionPath: collectionPath,
|
|
163
|
+
targetDefinition: targetDefinition,
|
|
164
|
+
displayField: useAsTitle,
|
|
165
|
+
isOpen: pickerOpen,
|
|
166
|
+
onSelect: handlePick,
|
|
167
|
+
onDismiss: ()=>setPickerOpen(false)
|
|
168
|
+
}),
|
|
169
|
+
/*#__PURE__*/ jsx("span", {
|
|
170
|
+
className: tree_placement_widget_module["sr-only"],
|
|
171
|
+
children: t("treeWidget.srDescription")
|
|
172
|
+
})
|
|
173
|
+
]
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
export { TreePlacementWidget };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import "./tree-placement-widget_module.css";
|
|
2
|
+
const tree_placement_widget_module = {
|
|
3
|
+
tree: "tree-QTfVWc",
|
|
4
|
+
heading: "heading-kMlzJM",
|
|
5
|
+
current: "current-V4s8B9",
|
|
6
|
+
parentLabel: "parentLabel-AOKJXk",
|
|
7
|
+
parentValue: "parentValue-EzOVWx",
|
|
8
|
+
root: "root-mvHqUc",
|
|
9
|
+
actions: "actions-e3yRzc",
|
|
10
|
+
link: "link-hHX4SE",
|
|
11
|
+
error: "error-SXo8E4",
|
|
12
|
+
"sr-only": "sr-only-iZPr5G",
|
|
13
|
+
srOnly: "sr-only-iZPr5G"
|
|
14
|
+
};
|
|
15
|
+
export default tree_placement_widget_module;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
:is(.tree-QTfVWc, .byline-form-tree) {
|
|
2
|
+
gap: var(--spacing-8);
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
display: flex;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
:is(.heading-kMlzJM, .byline-form-tree-heading) {
|
|
8
|
+
font-size: .875rem;
|
|
9
|
+
font-weight: 600;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
:is(.current-V4s8B9, .byline-form-tree-current) {
|
|
13
|
+
font-size: .875rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.parentLabel-AOKJXk {
|
|
17
|
+
color: var(--gray-500);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.parentValue-EzOVWx {
|
|
21
|
+
font-weight: 500;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.root-mvHqUc {
|
|
25
|
+
color: var(--gray-500);
|
|
26
|
+
font-style: italic;
|
|
27
|
+
font-weight: 400;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
:is(.actions-e3yRzc, .byline-form-tree-actions) {
|
|
31
|
+
flex-wrap: wrap;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: .75rem;
|
|
34
|
+
display: flex;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
:is(.link-hHX4SE, .byline-form-tree-link) {
|
|
38
|
+
color: inherit;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
background: none;
|
|
41
|
+
border: none;
|
|
42
|
+
padding: 0;
|
|
43
|
+
font-size: .8rem;
|
|
44
|
+
text-decoration: underline;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:is(.link-hHX4SE:disabled, .byline-form-tree-link:disabled) {
|
|
48
|
+
opacity: .5;
|
|
49
|
+
cursor: default;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
:is(.error-SXo8E4, .byline-form-tree-error) {
|
|
53
|
+
color: var(--danger-600, #c0392b);
|
|
54
|
+
margin: 0;
|
|
55
|
+
font-size: .8rem;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.sr-only-iZPr5G {
|
|
59
|
+
clip: rect(0, 0, 0, 0);
|
|
60
|
+
white-space: nowrap;
|
|
61
|
+
border: 0;
|
|
62
|
+
width: 1px;
|
|
63
|
+
height: 1px;
|
|
64
|
+
margin: -1px;
|
|
65
|
+
padding: 0;
|
|
66
|
+
position: absolute;
|
|
67
|
+
overflow: hidden;
|
|
68
|
+
}
|
|
69
|
+
|
package/dist/react.d.ts
CHANGED
|
@@ -64,4 +64,4 @@ export * from './presentation/tabs.js';
|
|
|
64
64
|
export * from './widgets/diff-viewer/diff-modal.js';
|
|
65
65
|
export * from './widgets/source-locale-badge/source-locale-badge.js';
|
|
66
66
|
export * from './widgets/status-badge/status-badge.js';
|
|
67
|
-
export type { BylineFieldServices, CollectionListDoc, CollectionListParams, CollectionListResponse, GetCollectionDocumentsFn, UploadedFileResult, UploadFieldFn, } from './fields/field-services-types.js';
|
|
67
|
+
export type { BylineFieldServices, CollectionListDoc, CollectionListParams, CollectionListResponse, GetCollectionDocumentsFn, 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": "3.
|
|
5
|
+
"version": "3.13.1",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -151,10 +151,10 @@
|
|
|
151
151
|
"uuid": "^14.0.0",
|
|
152
152
|
"zod": "^4.4.3",
|
|
153
153
|
"zod-form-data": "^3.0.1",
|
|
154
|
-
"@byline/auth": "3.
|
|
155
|
-
"@byline/
|
|
156
|
-
"@byline/
|
|
157
|
-
"@byline/
|
|
154
|
+
"@byline/auth": "3.13.1",
|
|
155
|
+
"@byline/i18n": "3.13.1",
|
|
156
|
+
"@byline/ui": "3.13.1",
|
|
157
|
+
"@byline/core": "3.13.1"
|
|
158
158
|
},
|
|
159
159
|
"peerDependencies": {
|
|
160
160
|
"react": "^19.0.0",
|
|
@@ -62,7 +62,57 @@ export type UploadFieldFn = (
|
|
|
62
62
|
createDocument?: boolean
|
|
63
63
|
) => Promise<UploadedFileResult>
|
|
64
64
|
|
|
65
|
+
// --- Document tree (the `tree: true` primitive — docs/DOCUMENT-TREE.md) -----
|
|
66
|
+
|
|
67
|
+
/** One hydrated ancestor in a document's breadcrumb trail (root-first). */
|
|
68
|
+
export interface TreeAncestor {
|
|
69
|
+
id: string
|
|
70
|
+
title: string
|
|
71
|
+
path?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PlaceTreeNodeInput {
|
|
75
|
+
collection: string
|
|
76
|
+
documentId: string
|
|
77
|
+
/** The new parent; `null` makes the document a root node. */
|
|
78
|
+
parentDocumentId: string | null
|
|
79
|
+
/** Optional sibling neighbours (left = land after, right = land before). */
|
|
80
|
+
beforeDocumentId?: string | null
|
|
81
|
+
afterDocumentId?: string | null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Place / move a document within its collection's tree. */
|
|
85
|
+
export type PlaceTreeNodeFn = (input: PlaceTreeNodeInput) => Promise<{ orderKey: string }>
|
|
86
|
+
|
|
87
|
+
/** Remove a document from the tree (back to the unplaced state). */
|
|
88
|
+
export type RemoveFromTreeFn = (input: { collection: string; documentId: string }) => Promise<void>
|
|
89
|
+
|
|
90
|
+
/** Resolve a document's ancestor chain, root-first, hydrated with titles. */
|
|
91
|
+
export type GetTreeAncestorsFn = (input: {
|
|
92
|
+
collection: string
|
|
93
|
+
documentId: string
|
|
94
|
+
}) => Promise<TreeAncestor[]>
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve a document's placement state — the tri-state (unplaced / root / child)
|
|
98
|
+
* that `getTreeAncestors` cannot express (it returns `[]` for both root and
|
|
99
|
+
* unplaced). `placed: false` = unplaced; `placed: true` + null parent = root.
|
|
100
|
+
*/
|
|
101
|
+
export type GetTreeParentFn = (input: {
|
|
102
|
+
collection: string
|
|
103
|
+
documentId: string
|
|
104
|
+
}) => Promise<{ placed: boolean; parentDocumentId: string | null }>
|
|
105
|
+
|
|
65
106
|
export interface BylineFieldServices {
|
|
66
107
|
getCollectionDocuments: GetCollectionDocumentsFn
|
|
67
108
|
uploadField: UploadFieldFn
|
|
109
|
+
/**
|
|
110
|
+
* Document-tree operations, consumed by the sidebar tree-placement widget.
|
|
111
|
+
* Optional — only hosts that serve `tree: true` collections need to wire
|
|
112
|
+
* them; the widget guards on their presence.
|
|
113
|
+
*/
|
|
114
|
+
placeTreeNode?: PlaceTreeNodeFn
|
|
115
|
+
removeFromTree?: RemoveFromTreeFn
|
|
116
|
+
getTreeAncestors?: GetTreeAncestorsFn
|
|
117
|
+
getTreeParent?: GetTreeParentFn
|
|
68
118
|
}
|
|
@@ -37,6 +37,7 @@ import { FormStatusDisplay } from './form-status-display'
|
|
|
37
37
|
import { useNavigationGuardAdapter } from './navigation-guard'
|
|
38
38
|
import { PathWidget } from './path-widget'
|
|
39
39
|
import { computeStatusTransitions } from './status-transitions'
|
|
40
|
+
import { TreePlacementWidget } from './tree-placement-widget'
|
|
40
41
|
import { executeUploadsWithProgress } from './upload-executor'
|
|
41
42
|
import { useFormLayout } from './use-form-layout'
|
|
42
43
|
import type { UseNavigationGuard } from './navigation-guard'
|
|
@@ -129,6 +130,13 @@ export interface FormRendererProps {
|
|
|
129
130
|
* against the document's `_availableVersionLocales` ledger fact.
|
|
130
131
|
*/
|
|
131
132
|
advertiseLocales?: boolean
|
|
133
|
+
/**
|
|
134
|
+
* Opts the document-tree placement widget into the sidebar (above the
|
|
135
|
+
* available-locales widget). Sourced from `CollectionDefinition.tree` by the
|
|
136
|
+
* caller. Renders only in edit mode (placement needs a persisted document)
|
|
137
|
+
* and only when the host wires the tree services. See docs/DOCUMENT-TREE.md.
|
|
138
|
+
*/
|
|
139
|
+
tree?: boolean
|
|
132
140
|
headingLabel?: string
|
|
133
141
|
headerSlot?: ReactNode
|
|
134
142
|
/** Collection path forwarded to upload-capable fields (e.g. `'media'`). */
|
|
@@ -179,6 +187,7 @@ const FormContent = ({
|
|
|
179
187
|
useAsTitle,
|
|
180
188
|
useAsPath,
|
|
181
189
|
advertiseLocales,
|
|
190
|
+
tree,
|
|
182
191
|
headingLabel,
|
|
183
192
|
headerSlot,
|
|
184
193
|
collectionPath,
|
|
@@ -665,6 +674,13 @@ const FormContent = ({
|
|
|
665
674
|
mode={mode}
|
|
666
675
|
/>
|
|
667
676
|
)}
|
|
677
|
+
{tree && mode === 'edit' && typeof initialData?.id === 'string' && (
|
|
678
|
+
<TreePlacementWidget
|
|
679
|
+
collectionPath={collectionPath ?? ''}
|
|
680
|
+
documentId={initialData.id as string}
|
|
681
|
+
useAsTitle={useAsTitle}
|
|
682
|
+
/>
|
|
683
|
+
)}
|
|
668
684
|
{advertiseLocales && (
|
|
669
685
|
<AvailableLocalesWidget
|
|
670
686
|
contentLocales={contentLocales ?? []}
|
|
@@ -715,6 +731,7 @@ export const FormRenderer = ({
|
|
|
715
731
|
useAsTitle,
|
|
716
732
|
useAsPath,
|
|
717
733
|
advertiseLocales,
|
|
734
|
+
tree,
|
|
718
735
|
headingLabel,
|
|
719
736
|
headerSlot,
|
|
720
737
|
collectionPath,
|
|
@@ -753,6 +770,7 @@ export const FormRenderer = ({
|
|
|
753
770
|
useAsTitle={useAsTitle}
|
|
754
771
|
useAsPath={useAsPath}
|
|
755
772
|
advertiseLocales={advertiseLocales}
|
|
773
|
+
tree={tree}
|
|
756
774
|
headingLabel={headingLabel}
|
|
757
775
|
headerSlot={headerSlot}
|
|
758
776
|
collectionPath={collectionPath}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TreePlacementWidget — sidebar widget for placing a document within its
|
|
3
|
+
* collection's single-parent tree (the `tree: true` primitive).
|
|
4
|
+
*
|
|
5
|
+
* Override handles:
|
|
6
|
+
* .byline-form-tree — wrapper div
|
|
7
|
+
* .byline-form-tree-current — current-parent line
|
|
8
|
+
* .byline-form-tree-actions — actions row
|
|
9
|
+
* .byline-form-tree-link — text-button actions (move to top level / remove)
|
|
10
|
+
* .byline-form-tree-error — inline error
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
.tree,
|
|
14
|
+
:global(.byline-form-tree) {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: var(--spacing-8);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.heading,
|
|
21
|
+
:global(.byline-form-tree-heading) {
|
|
22
|
+
font-size: 0.875rem;
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.current,
|
|
27
|
+
:global(.byline-form-tree-current) {
|
|
28
|
+
font-size: 0.875rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.parentLabel {
|
|
32
|
+
color: var(--gray-500);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.parentValue {
|
|
36
|
+
font-weight: 500;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.root {
|
|
40
|
+
font-style: italic;
|
|
41
|
+
font-weight: 400;
|
|
42
|
+
color: var(--gray-500);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.actions,
|
|
46
|
+
:global(.byline-form-tree-actions) {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
flex-wrap: wrap;
|
|
50
|
+
gap: 0.75rem;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.link,
|
|
54
|
+
:global(.byline-form-tree-link) {
|
|
55
|
+
background: none;
|
|
56
|
+
border: none;
|
|
57
|
+
padding: 0;
|
|
58
|
+
color: inherit;
|
|
59
|
+
font-size: 0.8rem;
|
|
60
|
+
text-decoration: underline;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.link:disabled,
|
|
65
|
+
:global(.byline-form-tree-link):disabled {
|
|
66
|
+
opacity: 0.5;
|
|
67
|
+
cursor: default;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.error,
|
|
71
|
+
:global(.byline-form-tree-error) {
|
|
72
|
+
margin: 0;
|
|
73
|
+
font-size: 0.8rem;
|
|
74
|
+
color: var(--danger-600, #c0392b);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.sr-only {
|
|
78
|
+
position: absolute;
|
|
79
|
+
width: 1px;
|
|
80
|
+
height: 1px;
|
|
81
|
+
padding: 0;
|
|
82
|
+
margin: -1px;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
clip: rect(0, 0, 0, 0);
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
border: 0;
|
|
87
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
5
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
7
|
+
*
|
|
8
|
+
* Copyright (c) Infonomic Company Limited
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
12
|
+
|
|
13
|
+
import { getCollectionDefinition } from '@byline/core'
|
|
14
|
+
import { useTranslation } from '@byline/i18n/react'
|
|
15
|
+
import { Button } from '@byline/ui/react'
|
|
16
|
+
import cx from 'classnames'
|
|
17
|
+
|
|
18
|
+
import { useBylineFieldServices } from '../fields/field-services-context.js'
|
|
19
|
+
import { RelationPicker } from '../fields/relation/relation-picker.js'
|
|
20
|
+
import styles from './tree-placement-widget.module.css'
|
|
21
|
+
|
|
22
|
+
export interface TreePlacementWidgetProps {
|
|
23
|
+
/** The collection path (`tree: true`). */
|
|
24
|
+
collectionPath: string
|
|
25
|
+
/** The logical id of the document being edited. */
|
|
26
|
+
documentId: string
|
|
27
|
+
/** The collection's `useAsTitle` field, used to label the chosen parent. */
|
|
28
|
+
useAsTitle?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sidebar widget for placing the current document within its collection's
|
|
33
|
+
* single-parent document tree (the `tree: true` primitive — docs/DOCUMENT-TREE.md).
|
|
34
|
+
*
|
|
35
|
+
* The tree is document-grain and **unversioned**, so changes here write
|
|
36
|
+
* immediately (independent of the form's content save). The editor picks a
|
|
37
|
+
* parent through the collection's own relation picker — same search / columns
|
|
38
|
+
* UX as a relation field — or moves the document to the top level; the server
|
|
39
|
+
* enforces the cycle / same-collection invariants and fires the
|
|
40
|
+
* structural-change invalidation event.
|
|
41
|
+
*
|
|
42
|
+
* Renders only in edit mode (placement needs a persisted document) and only when
|
|
43
|
+
* the host wires the tree services. Stable override handle: `.byline-form-tree`.
|
|
44
|
+
*/
|
|
45
|
+
export const TreePlacementWidget = ({
|
|
46
|
+
collectionPath,
|
|
47
|
+
documentId,
|
|
48
|
+
useAsTitle,
|
|
49
|
+
}: TreePlacementWidgetProps) => {
|
|
50
|
+
const { t } = useTranslation('byline-admin')
|
|
51
|
+
const { getTreeAncestors, getTreeParent, placeTreeNode } = useBylineFieldServices()
|
|
52
|
+
const targetDefinition = getCollectionDefinition(collectionPath)
|
|
53
|
+
|
|
54
|
+
const [parent, setParent] = useState<{ id: string; title: string } | null>(null)
|
|
55
|
+
// Whether the document has an edge row at all. `false` = *unplaced* (no row);
|
|
56
|
+
// `true` with a null `parent` = a *root*. Distinguishing the two drives the
|
|
57
|
+
// "Add to tree" (unplaced) vs "Top level" (root) display. Hosts that don't
|
|
58
|
+
// wire `getTreeParent` fall back to `true` — the pre-tri-state behaviour.
|
|
59
|
+
const [placed, setPlaced] = useState(true)
|
|
60
|
+
const [loading, setLoading] = useState(true)
|
|
61
|
+
const [busy, setBusy] = useState(false)
|
|
62
|
+
const [error, setError] = useState<string | null>(null)
|
|
63
|
+
const [pickerOpen, setPickerOpen] = useState(false)
|
|
64
|
+
|
|
65
|
+
const treeServicesReady = getTreeAncestors != null && placeTreeNode != null
|
|
66
|
+
|
|
67
|
+
// Load the document's placement state and current parent on mount. The parent
|
|
68
|
+
// title comes from the (hydrated) ancestor chain; `getTreeParent` (when wired)
|
|
69
|
+
// tells unplaced from root, which the ancestor chain alone cannot.
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (getTreeAncestors == null) return
|
|
72
|
+
let cancelled = false
|
|
73
|
+
setLoading(true)
|
|
74
|
+
Promise.all([
|
|
75
|
+
getTreeAncestors({ collection: collectionPath, documentId }),
|
|
76
|
+
getTreeParent?.({ collection: collectionPath, documentId }) ??
|
|
77
|
+
Promise.resolve({ placed: true, parentDocumentId: null }),
|
|
78
|
+
])
|
|
79
|
+
.then(([ancestors, placement]) => {
|
|
80
|
+
if (cancelled) return
|
|
81
|
+
const immediate = ancestors.at(-1)
|
|
82
|
+
setParent(immediate ? { id: immediate.id, title: immediate.title } : null)
|
|
83
|
+
setPlaced(placement.placed)
|
|
84
|
+
})
|
|
85
|
+
.catch(() => {
|
|
86
|
+
if (!cancelled) setError(t('treeWidget.error'))
|
|
87
|
+
})
|
|
88
|
+
.finally(() => {
|
|
89
|
+
if (!cancelled) setLoading(false)
|
|
90
|
+
})
|
|
91
|
+
return () => {
|
|
92
|
+
cancelled = true
|
|
93
|
+
}
|
|
94
|
+
}, [getTreeAncestors, getTreeParent, collectionPath, documentId, t])
|
|
95
|
+
|
|
96
|
+
// Place (or re-parent) the node, optimistically updating the current-parent
|
|
97
|
+
// display and reverting on a server rejection (e.g. a cycle). Any successful
|
|
98
|
+
// placement (including making the node a root) leaves it placed.
|
|
99
|
+
const place = useCallback(
|
|
100
|
+
async (parentDocumentId: string | null, optimistic: { id: string; title: string } | null) => {
|
|
101
|
+
if (placeTreeNode == null || busy) return
|
|
102
|
+
const previousParent = parent
|
|
103
|
+
const previousPlaced = placed
|
|
104
|
+
setError(null)
|
|
105
|
+
setBusy(true)
|
|
106
|
+
setParent(optimistic)
|
|
107
|
+
setPlaced(true)
|
|
108
|
+
try {
|
|
109
|
+
await placeTreeNode({ collection: collectionPath, documentId, parentDocumentId })
|
|
110
|
+
} catch {
|
|
111
|
+
setParent(previousParent)
|
|
112
|
+
setPlaced(previousPlaced)
|
|
113
|
+
setError(t('treeWidget.error'))
|
|
114
|
+
} finally {
|
|
115
|
+
setBusy(false)
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
[placeTreeNode, busy, parent, placed, collectionPath, documentId, t]
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
const handlePick = useCallback(
|
|
122
|
+
(selection: { targetDocumentId: string; record?: Record<string, any> }) => {
|
|
123
|
+
setPickerOpen(false)
|
|
124
|
+
const title = useAsTitle ? selection.record?.[useAsTitle] : undefined
|
|
125
|
+
place(selection.targetDocumentId, {
|
|
126
|
+
id: selection.targetDocumentId,
|
|
127
|
+
title: typeof title === 'string' && title.length > 0 ? title : selection.targetDocumentId,
|
|
128
|
+
})
|
|
129
|
+
},
|
|
130
|
+
[place, useAsTitle]
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if (!treeServicesReady) return null
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className={cx('byline-form-tree', styles.tree)}>
|
|
137
|
+
<span className={cx('byline-form-tree-heading', styles.heading)}>
|
|
138
|
+
{t('treeWidget.label')}
|
|
139
|
+
</span>
|
|
140
|
+
|
|
141
|
+
<div className={cx('byline-form-tree-current', styles.current)}>
|
|
142
|
+
<span className={styles.parentLabel}>{t('treeWidget.parentPrefix')}</span>{' '}
|
|
143
|
+
{!placed ? (
|
|
144
|
+
<span className={cx(styles.parentValue, styles.root)}>{t('treeWidget.notInTree')}</span>
|
|
145
|
+
) : parent ? (
|
|
146
|
+
<span className={styles.parentValue}>{parent.title}</span>
|
|
147
|
+
) : (
|
|
148
|
+
<span className={cx(styles.parentValue, styles.root)}>{t('treeWidget.rootOption')}</span>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div className={cx('byline-form-tree-actions', styles.actions)}>
|
|
153
|
+
<Button
|
|
154
|
+
type="button"
|
|
155
|
+
size="xs"
|
|
156
|
+
variant="outlined"
|
|
157
|
+
intent="noeffect"
|
|
158
|
+
disabled={loading || busy}
|
|
159
|
+
onClick={() => setPickerOpen(true)}
|
|
160
|
+
>
|
|
161
|
+
{t('treeWidget.choose')}
|
|
162
|
+
</Button>
|
|
163
|
+
{!placed ? (
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
className={cx('byline-form-tree-link', styles.link)}
|
|
167
|
+
disabled={loading || busy}
|
|
168
|
+
onClick={() => place(null, null)}
|
|
169
|
+
>
|
|
170
|
+
{t('treeWidget.addToTree')}
|
|
171
|
+
</button>
|
|
172
|
+
) : (
|
|
173
|
+
parent != null && (
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
className={cx('byline-form-tree-link', styles.link)}
|
|
177
|
+
disabled={busy}
|
|
178
|
+
onClick={() => place(null, null)}
|
|
179
|
+
>
|
|
180
|
+
{t('treeWidget.makeRoot')}
|
|
181
|
+
</button>
|
|
182
|
+
)
|
|
183
|
+
)}
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{error != null && <p className={cx('byline-form-tree-error', styles.error)}>{error}</p>}
|
|
187
|
+
|
|
188
|
+
{targetDefinition != null && (
|
|
189
|
+
<RelationPicker
|
|
190
|
+
targetCollectionPath={collectionPath}
|
|
191
|
+
targetDefinition={targetDefinition}
|
|
192
|
+
displayField={useAsTitle}
|
|
193
|
+
isOpen={pickerOpen}
|
|
194
|
+
onSelect={handlePick}
|
|
195
|
+
onDismiss={() => setPickerOpen(false)}
|
|
196
|
+
/>
|
|
197
|
+
)}
|
|
198
|
+
|
|
199
|
+
<span className={styles['sr-only']}>{t('treeWidget.srDescription')}</span>
|
|
200
|
+
</div>
|
|
201
|
+
)
|
|
202
|
+
}
|
package/src/react.ts
CHANGED
|
@@ -80,6 +80,12 @@ export type {
|
|
|
80
80
|
CollectionListParams,
|
|
81
81
|
CollectionListResponse,
|
|
82
82
|
GetCollectionDocumentsFn,
|
|
83
|
+
GetTreeAncestorsFn,
|
|
84
|
+
GetTreeParentFn,
|
|
85
|
+
PlaceTreeNodeFn,
|
|
86
|
+
PlaceTreeNodeInput,
|
|
87
|
+
RemoveFromTreeFn,
|
|
88
|
+
TreeAncestor,
|
|
83
89
|
UploadedFileResult,
|
|
84
90
|
UploadFieldFn,
|
|
85
91
|
} from './fields/field-services-types.js'
|