@sanity/hierarchical-document-list 0.1.0-next.1 → 0.1.0-next.2
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/README.md +45 -33
- package/lib/TreeDeskStructure.js +14 -10
- package/lib/TreeInputComponent.d.ts +3 -3
- package/lib/components/DeskWarning.d.ts +6 -0
- package/lib/components/DeskWarning.js +12 -0
- package/lib/components/DocumentInNode.d.ts +2 -2
- package/lib/components/NodeActions.d.ts +2 -2
- package/lib/components/NodeActions.js +3 -2
- package/lib/components/TreeEditor.d.ts +2 -2
- package/lib/components/TreeEditor.js +1 -1
- package/lib/createDeskHierarchy.d.ts +4 -0
- package/lib/createDeskHierarchy.js +2 -1
- package/lib/createHierarchicalField.js +12 -6
- package/lib/{hiearchy.tree.d.ts → schemas/hiearchy.tree.d.ts} +0 -0
- package/lib/{hiearchy.tree.js → schemas/hiearchy.tree.js} +3 -1
- package/lib/types.d.ts +122 -0
- package/lib/types.js +1 -0
- package/lib/utils/flatDataToTree.d.ts +3 -3
- package/lib/utils/treeData.d.ts +7 -7
- package/lib/utils/treePatches.d.ts +10 -8
- package/lib/utils/treePatches.js +3 -14
- package/lib/utils/useLocalTree.d.ts +5 -5
- package/lib/utils/useLocalTree.js +1 -1
- package/lib/utils/useTreeOperationsProvider.d.ts +7 -8
- package/package.json +11 -9
- package/sanity.json +1 -1
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ Plugin for editing hierarchical references in the [Sanity studio](https://www.sa
|
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
+
⚠️ **Compatibility:** This plugin requires Sanity Studio [version 2.25.0](https://github.com/sanity-io/sanity/releases/tag/v2.25.0) or higher.
|
|
8
|
+
|
|
7
9
|
## Getting started
|
|
8
10
|
|
|
9
11
|
```bash
|
|
@@ -28,7 +30,7 @@ export default () => {
|
|
|
28
30
|
createDeskHierarchy({
|
|
29
31
|
title: 'Main table of contents',
|
|
30
32
|
|
|
31
|
-
// The
|
|
33
|
+
// The hierarchy will be stored in this document ID 👇
|
|
32
34
|
documentId: 'main-table-of-contents',
|
|
33
35
|
|
|
34
36
|
// Document types editors should be able to include in the hierarchy
|
|
@@ -65,20 +67,26 @@ The plugin stores flat arrays which represent your hierarchical data through `pa
|
|
|
65
67
|
{
|
|
66
68
|
"_key": "741b9edde2ba",
|
|
67
69
|
"_type": "hierarchy.node",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
"value": {
|
|
71
|
+
"reference": {
|
|
72
|
+
"_ref": "75c47994-e6bb-487a-b8c9-b283f2436031",
|
|
73
|
+
"_type": "reference",
|
|
74
|
+
"_weak": true // This plugin includes weak references by default
|
|
75
|
+
},
|
|
76
|
+
"docType": "docs.article"
|
|
72
77
|
}
|
|
73
78
|
// no `parent`, this item is top-level
|
|
74
79
|
},
|
|
75
80
|
{
|
|
76
81
|
"_key": "f92eaeec96f7",
|
|
77
82
|
"_type": "hierarchy.node",
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
"value": {
|
|
84
|
+
"reference": {
|
|
85
|
+
"_ref": "7ad60a02-5d6e-47d8-92e2-6724cc130058",
|
|
86
|
+
"_type": "reference",
|
|
87
|
+
"_weak": true
|
|
88
|
+
},
|
|
89
|
+
"docType": "site.post"
|
|
82
90
|
},
|
|
83
91
|
// The `parent` property points to the _key of the parent node where this one is nested
|
|
84
92
|
"parent": "741b9edde2ba"
|
|
@@ -98,10 +106,12 @@ From the the above, we know how to expand referenced documents in GROQ:
|
|
|
98
106
|
parent,
|
|
99
107
|
|
|
100
108
|
// "Expand" the reference to the node
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
109
|
+
value {
|
|
110
|
+
reference->{
|
|
111
|
+
// Get whatever property you need from your documents
|
|
112
|
+
title,
|
|
113
|
+
slug,
|
|
114
|
+
}
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
}
|
|
@@ -119,13 +129,13 @@ Find a given document in a hierarchy and get its parent - useful for rendering b
|
|
|
119
129
|
// From the tree, get the 1st node that references a given document _id
|
|
120
130
|
tree[node._ref == "my-book-section"][0] {
|
|
121
131
|
_key,
|
|
122
|
-
"section": node->{
|
|
132
|
+
"section": node.reference->{
|
|
123
133
|
title,
|
|
124
134
|
},
|
|
125
135
|
// Then, from the tree get the element matching the `parent` _key of the found node
|
|
126
136
|
"parentChapter": ^.tree[_key == ^.parent][0]{
|
|
127
137
|
_key,
|
|
128
|
-
"chapter": node->{
|
|
138
|
+
"chapter": node.reference->{
|
|
129
139
|
title,
|
|
130
140
|
contributors,
|
|
131
141
|
}
|
|
@@ -148,10 +158,12 @@ const hierarchyDocument = await client.fetch(`*[_id == "book-v3-review-a"][0]{
|
|
|
148
158
|
// Make sure you include each item's _key and parent
|
|
149
159
|
_key,
|
|
150
160
|
parent,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
value {
|
|
162
|
+
reference->{
|
|
163
|
+
title,
|
|
164
|
+
slug,
|
|
165
|
+
content,
|
|
166
|
+
}
|
|
155
167
|
}
|
|
156
168
|
}
|
|
157
169
|
}`)
|
|
@@ -173,9 +185,10 @@ export default {
|
|
|
173
185
|
name: 'myCustomHierarchicalType',
|
|
174
186
|
title: 'Custom document type for holding hierarchical data',
|
|
175
187
|
type: 'document',
|
|
188
|
+
liveEdit: true, // 👉 Important: set liveEdit to `true` to ensure the UI works properly
|
|
176
189
|
fields: [
|
|
177
190
|
createHierarchicalField({
|
|
178
|
-
name: '
|
|
191
|
+
name: 'customTreeDataKey', // key for the tree field in the document
|
|
179
192
|
title: 'Custom tree',
|
|
180
193
|
options: {
|
|
181
194
|
referenceTo: ['category']
|
|
@@ -185,26 +198,25 @@ export default {
|
|
|
185
198
|
}
|
|
186
199
|
```
|
|
187
200
|
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
📌 **Note:** you can also use the method above to add hierarchies inside the schema of documents and objects. We're considering adapting this input to support any type of nest-able data, not only references. Until then, avoid `createHierarchicalField` for fields in nested schemas as, in these contexts, it lacks the necessary affordances for a good editing experience.
|
|
191
|
-
|
|
192
|
-
---
|
|
193
|
-
|
|
194
201
|
Then, in your desk structure where you added the hierarchical document(s), include the right `documentType` and `fieldKeyInDocument` properties:
|
|
195
202
|
|
|
196
203
|
```js
|
|
197
204
|
createDeskHierarchy({
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
// Include whatever values you defined in your schema
|
|
203
|
-
documentType: 'myCustomHierarchicalType',
|
|
204
|
-
fieldKeyInDocument: 'treeData'
|
|
205
|
+
// Include whatever values you defined in your schema in the step above
|
|
206
|
+
documentType: 'myCustomHierarchicalType', // the name of your document type
|
|
207
|
+
fieldKeyInDocument: 'customTreeDataKey' // the name of the hierarchical field
|
|
208
|
+
// ...
|
|
205
209
|
})
|
|
206
210
|
```
|
|
207
211
|
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
📌 **Note:** you can also use the method above to add hierarchies inside the schema of documents and objects, which would be editable outside the desk structure.
|
|
215
|
+
|
|
216
|
+
We're considering adapting this input to support any type of nest-able data, not only references. Until then, avoid `createHierarchicalField` for fields in nested schemas as, in these contexts, it lacks the necessary affordances for a good editing experience.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
208
220
|
## License
|
|
209
221
|
|
|
210
222
|
MIT-licensed. See LICENSE.
|
package/lib/TreeDeskStructure.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { PublishIcon } from '@sanity/icons';
|
|
3
3
|
import { useDocumentOperation, useEditState } from '@sanity/react-hooks';
|
|
4
|
-
import { Box, Button,
|
|
4
|
+
import { Box, Button, Flex, Spinner, useToast } from '@sanity/ui';
|
|
5
5
|
import React from 'react';
|
|
6
|
+
import DeskWarning from './components/DeskWarning';
|
|
6
7
|
import TreeEditor from './components/TreeEditor';
|
|
7
8
|
import { toGradient } from './utils/gradientPatchAdapter';
|
|
8
9
|
const DEFAULT_TREE_FIELD_KEY = 'tree';
|
|
@@ -10,7 +11,7 @@ const DEFAULT_TREE_DOC_TYPE = 'hierarchy.tree';
|
|
|
10
11
|
const TreeDeskStructure = (props) => {
|
|
11
12
|
const treeDocType = props.options.documentType || DEFAULT_TREE_DOC_TYPE;
|
|
12
13
|
const treeFieldKey = props.options.fieldKeyInDocument || DEFAULT_TREE_FIELD_KEY;
|
|
13
|
-
const { published, draft } = useEditState(props.options.documentId, treeDocType);
|
|
14
|
+
const { published, draft, liveEdit } = useEditState(props.options.documentId, treeDocType);
|
|
14
15
|
const { patch, ...ops } = useDocumentOperation(props.options.documentId, treeDocType);
|
|
15
16
|
const { push } = useToast();
|
|
16
17
|
const treeValue = (published?.[treeFieldKey] || []);
|
|
@@ -26,14 +27,17 @@ const TreeDeskStructure = (props) => {
|
|
|
26
27
|
patch.execute([{ setIfMissing: { [treeFieldKey]: [] } }]);
|
|
27
28
|
}
|
|
28
29
|
}, [published?._id, patch]);
|
|
30
|
+
if (!liveEdit) {
|
|
31
|
+
return (_jsx(DeskWarning, { title: "Invalid configuration", subtitle: "The `documentType` passed to `createDeskHiearchy` isn't live editable. \\nTo continue using this plugin, add `liveEdit: true` to your custom schema type or unset `documentType` in your hierarchy configuration." }, void 0));
|
|
32
|
+
}
|
|
29
33
|
if (draft?._id) {
|
|
30
|
-
return (_jsx(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
return (_jsx(DeskWarning, { title: "This hierarchy tree contains a draft", subtitle: "Click on the button below to publish your draft in order to continue editing the live\n published document.", children: _jsx(Button, { fontSize: 1, tone: "positive", text: "Publish draft", icon: PublishIcon, onClick: () => {
|
|
35
|
+
ops.publish?.execute?.();
|
|
36
|
+
push({
|
|
37
|
+
status: 'info',
|
|
38
|
+
title: 'Publishing draft...'
|
|
39
|
+
});
|
|
40
|
+
} }, void 0) }, void 0));
|
|
37
41
|
}
|
|
38
42
|
if (!published?._id) {
|
|
39
43
|
return (_jsx(Flex, { padding: 5, align: 'center', justify: 'center', height: 'fill', children: _jsx(Spinner, { width: 4, muted: true }, void 0) }, void 0));
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { FormFieldPresence } from '@sanity/base/presence';
|
|
2
2
|
import { Marker, Path } from '@sanity/types';
|
|
3
3
|
import React from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { StoredTreeItem, TreeFieldSchema } from './types';
|
|
5
5
|
export interface TreeInputComponentProps {
|
|
6
6
|
type: TreeFieldSchema;
|
|
7
|
-
value:
|
|
8
|
-
compareValue:
|
|
7
|
+
value: StoredTreeItem[];
|
|
8
|
+
compareValue: StoredTreeItem[];
|
|
9
9
|
markers: Marker[];
|
|
10
10
|
level: number;
|
|
11
11
|
onChange: (event: unknown) => void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Card, Container, Heading, Stack, Text } from '@sanity/ui';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
// React component that wraps text between two delimiters in a <pre> tag
|
|
5
|
+
const WrapCodeBlocks = ({ text }) => {
|
|
6
|
+
return (_jsx(_Fragment, { children: text.split('`').map((part, i) => (_jsx(React.Fragment, { children: i % 2 === 0 ? part : _jsx("code", { children: part }, void 0) }, i))) }, void 0));
|
|
7
|
+
};
|
|
8
|
+
const DeskWarning = ({ subtitle, title, children }) => {
|
|
9
|
+
return (_jsx(Container, { padding: 5, style: { maxWidth: '25rem' }, sizing: 'content', children: _jsx(Card, { padding: 4, border: true, radius: 2, width: 0, tone: "caution", children: _jsxs(Stack, { space: 3, children: [_jsx(Heading, { size: 1, children: title }, void 0), subtitle &&
|
|
10
|
+
subtitle.split('\\n').map((line) => (_jsx(Text, { size: 1, children: _jsx(WrapCodeBlocks, { text: line }, void 0) }, void 0))), children && _jsx(Box, { marginTop: 2, children: children }, void 0)] }, void 0) }, void 0) }, void 0));
|
|
11
|
+
};
|
|
12
|
+
export default DeskWarning;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { LocalTreeItem } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* Renders a preview for each referenced document.
|
|
5
5
|
* Nested inside TreeNode.tsx
|
|
6
6
|
*/
|
|
7
7
|
declare const DocumentInNode: React.FC<{
|
|
8
|
-
item:
|
|
8
|
+
item: LocalTreeItem;
|
|
9
9
|
action?: React.ReactNode;
|
|
10
10
|
}>;
|
|
11
11
|
export default DocumentInNode;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { NodeProps } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* Applicable only to nodes inside the main tree.
|
|
5
5
|
* Unadded items have their actions defined in TreeEditor.
|
|
6
6
|
*/
|
|
7
7
|
declare const NodeActions: React.FC<{
|
|
8
|
-
nodeProps:
|
|
8
|
+
nodeProps: NodeProps;
|
|
9
9
|
}>;
|
|
10
10
|
export default NodeActions;
|
|
@@ -11,12 +11,13 @@ import useTreeOperations from '../utils/useTreeOperations';
|
|
|
11
11
|
const NodeActions = ({ nodeProps }) => {
|
|
12
12
|
const operations = useTreeOperations();
|
|
13
13
|
const { node } = nodeProps;
|
|
14
|
+
const { reference, docType } = node?.value || {};
|
|
14
15
|
// Adapted from @sanity\form-builder\src\inputs\ReferenceInput\ArrayItemReferenceInput.tsx
|
|
15
16
|
const OpenLink = React.useMemo(() =>
|
|
16
17
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
17
18
|
React.forwardRef(function OpenLinkInner(restProps, _ref) {
|
|
18
|
-
return (_jsx(IntentLink, { ...restProps, intent: "edit", params: { id:
|
|
19
|
-
}), [
|
|
19
|
+
return (_jsx(IntentLink, { ...restProps, intent: "edit", params: { id: reference?._ref, type: docType }, target: "_blank", rel: "noopener noreferrer", ref: _ref }, void 0));
|
|
20
|
+
}), [reference?._ref, docType]);
|
|
20
21
|
const isValid = !!node.publishedId;
|
|
21
22
|
return (_jsx(MenuButton, { button: _jsx(Button, { padding: 2, mode: "bleed", icon: EllipsisVerticalIcon }, void 0), id: `hiearchical-doc-list--${node._key}-menuButton`, menu: _jsxs(Menu, { children: [_jsx(MenuItem, { text: "Remove from list", tone: "critical", icon: RemoveCircleIcon, onClick: () => operations.removeItem(nodeProps) }, void 0), _jsx(MenuItem, { text: "Duplicate", icon: CopyIcon, disabled: !isValid, onClick: () => operations.duplicateItem(nodeProps) }, void 0), _jsx(MenuDivider, {}, void 0), _jsx(MenuItem, { text: "Open in new tab", icon: LaunchIcon, disabled: !isValid, as: OpenLink, "data-as": "a" }, void 0)] }, void 0), placement: "right", popover: { portal: true, tone: 'default' } }, void 0));
|
|
22
23
|
};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { StoredTreeItem, TreeInputOptions } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* The loaded tree users interact with
|
|
5
5
|
*/
|
|
6
6
|
declare const TreeEditor: React.FC<{
|
|
7
|
-
tree:
|
|
7
|
+
tree: StoredTreeItem[];
|
|
8
8
|
onChange: (patch: unknown) => void;
|
|
9
9
|
options: TreeInputOptions;
|
|
10
10
|
patchPrefix?: string;
|
|
@@ -30,7 +30,7 @@ const TreeEditor = (props) => {
|
|
|
30
30
|
// Only include borderBottom if there's something to show in unadded items
|
|
31
31
|
borderBottom: allItemsStatus !== 'success' || unaddedItems?.length > 0, children: _jsx(SortableTree, { maxDepth: props.options.maxDepth, onChange: () => {
|
|
32
32
|
// Do nothing. onMoveNode will do all the work
|
|
33
|
-
}, onVisibilityToggle: handleVisibilityToggle, onMoveNode: operations.handleMovedNode, treeData: localTree, ...getCommonTreeProps({
|
|
33
|
+
}, onVisibilityToggle: handleVisibilityToggle, onMoveNode: (data) => operations.handleMovedNode(data), treeData: localTree, ...getCommonTreeProps({
|
|
34
34
|
placeholder: {
|
|
35
35
|
title: 'Add items by dragging them here'
|
|
36
36
|
}
|
|
@@ -5,6 +5,10 @@ interface TreeProps extends TreeDeskStructureProps {
|
|
|
5
5
|
* Also used as the label in the desk list item.
|
|
6
6
|
*/
|
|
7
7
|
title: string;
|
|
8
|
+
/**
|
|
9
|
+
* Optional icon for rendering the item in the desk structure.
|
|
10
|
+
*/
|
|
11
|
+
icon?: any;
|
|
8
12
|
}
|
|
9
13
|
export default function createDeskHierarchy(props: TreeProps): import("@sanity/structure/dist/dts/ListItem").ListItemBuilder;
|
|
10
14
|
export {};
|
|
@@ -16,7 +16,7 @@ export default function createDeskHierarchy(props) {
|
|
|
16
16
|
const { documentId, referenceTo, referenceOptions } = props;
|
|
17
17
|
let mainList = (referenceTo?.length === 1
|
|
18
18
|
? S.documentTypeList(referenceTo[0]).schemaType(referenceTo[0])
|
|
19
|
-
: S.documentList())
|
|
19
|
+
: S.documentList().filter('_type in $types').params({ types: referenceTo }))
|
|
20
20
|
.id(documentId)
|
|
21
21
|
.menuItems((referenceTo || []).map((schemaType) => S.menuItem()
|
|
22
22
|
.intent({
|
|
@@ -44,6 +44,7 @@ export default function createDeskHierarchy(props) {
|
|
|
44
44
|
return S.listItem()
|
|
45
45
|
.id(documentId)
|
|
46
46
|
.title(props.title || documentId)
|
|
47
|
+
.icon(props.icon)
|
|
47
48
|
.child(Object.assign(mainList.serialize(), {
|
|
48
49
|
type: 'component',
|
|
49
50
|
component: deskTreeValidator(props),
|
|
@@ -14,13 +14,19 @@ export default function createHierarchicalField({ name, title, options, ...rest
|
|
|
14
14
|
type: 'object',
|
|
15
15
|
fields: [
|
|
16
16
|
{ name: 'parent', type: 'string' },
|
|
17
|
-
{ name: 'nodeDocType', type: 'string' },
|
|
18
17
|
{
|
|
19
|
-
name: '
|
|
20
|
-
type: '
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
name: 'value',
|
|
19
|
+
type: 'object',
|
|
20
|
+
fields: [
|
|
21
|
+
{ name: 'docType', type: 'string' },
|
|
22
|
+
{
|
|
23
|
+
name: 'reference',
|
|
24
|
+
type: 'reference',
|
|
25
|
+
weak: true,
|
|
26
|
+
to: options.referenceTo.map((type) => ({ type })),
|
|
27
|
+
options: options.referenceOptions
|
|
28
|
+
}
|
|
29
|
+
]
|
|
24
30
|
}
|
|
25
31
|
]
|
|
26
32
|
}
|
|
File without changes
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import createHierarchicalField from '
|
|
1
|
+
import createHierarchicalField from '../createHierarchicalField';
|
|
2
2
|
export default {
|
|
3
3
|
name: 'hierarchy.tree',
|
|
4
4
|
title: 'Hierarchical tree',
|
|
5
5
|
type: 'document',
|
|
6
|
+
// The plugin needs to define a `schemaType` with liveEdit enabled so that
|
|
7
|
+
// `useDocumentOperation` in TreeDeskStructure.tsx doesn't create drafts at every patch.
|
|
6
8
|
liveEdit: true,
|
|
7
9
|
fields: [
|
|
8
10
|
createHierarchicalField({
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { SanityDocument } from '@sanity/client';
|
|
2
|
+
import { ArraySchemaType } from '@sanity/types';
|
|
3
|
+
import { NodeRendererProps, TreeItem } from 'react-sortable-tree';
|
|
4
|
+
interface SanityReference {
|
|
5
|
+
_type: 'reference';
|
|
6
|
+
_ref: string;
|
|
7
|
+
_weak?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Objects saved to tree documents in Sanity's Content Lake
|
|
11
|
+
*/
|
|
12
|
+
export interface StoredTreeItem {
|
|
13
|
+
_key: string;
|
|
14
|
+
_type: 'hierarchy.node' | string;
|
|
15
|
+
value?: {
|
|
16
|
+
reference?: SanityReference;
|
|
17
|
+
docType?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* _key of parent node
|
|
21
|
+
*/
|
|
22
|
+
parent?: string | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Tree items enhanced locally in the client with info from `allItems` and `visibilityMap`.
|
|
26
|
+
* `allItems` stop here and never become LocalTreeItems as they aren't added to react-sortable-tree.
|
|
27
|
+
*
|
|
28
|
+
* See `useLocalTree.ts` and `dataToEditorTree()`.
|
|
29
|
+
*/
|
|
30
|
+
export interface EnhancedTreeItem extends StoredTreeItem {
|
|
31
|
+
expanded?: boolean | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Used by DocumentInNode to render the preview for drafts if they exist.
|
|
34
|
+
* Also informs document status icons.
|
|
35
|
+
*/
|
|
36
|
+
draftId?: string;
|
|
37
|
+
draftUpdatedAt?: string;
|
|
38
|
+
/**
|
|
39
|
+
* If not present, DocumentInNode will show up an error for invalid document.
|
|
40
|
+
* - undefined `publishedId` could mean the document is either deleted, or it doesn't match GROQ filters anymore
|
|
41
|
+
*/
|
|
42
|
+
publishedId?: string;
|
|
43
|
+
publishedUpdatedAt?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Tree items as found in the sortable tree itself.
|
|
47
|
+
*/
|
|
48
|
+
export declare type LocalTreeItem = EnhancedTreeItem & Pick<TreeItem, 'title' | 'children'>;
|
|
49
|
+
export interface TreeInputOptions {
|
|
50
|
+
/**
|
|
51
|
+
* What document types this hierarchy can refer to.
|
|
52
|
+
* Similar to the `to` property of the [reference field](https://www.sanity.io/docs/reference-type).
|
|
53
|
+
*/
|
|
54
|
+
referenceTo: string[];
|
|
55
|
+
/**
|
|
56
|
+
* Used to provide fine-grained filtering for documents.
|
|
57
|
+
*/
|
|
58
|
+
referenceOptions?: {
|
|
59
|
+
/**
|
|
60
|
+
* Static filter to apply to tree document queries.
|
|
61
|
+
*/
|
|
62
|
+
filter?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Parameters / variables to pass to the GROQ query ran to fetch documents.
|
|
65
|
+
*/
|
|
66
|
+
filterParams?: Record<string, unknown>;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* How deep should editors be allowed to nest items.
|
|
70
|
+
*/
|
|
71
|
+
maxDepth?: number;
|
|
72
|
+
}
|
|
73
|
+
export interface TreeFieldSchema extends Omit<ArraySchemaType, 'of' | 'type' | 'inputComponent' | 'jsonType'> {
|
|
74
|
+
options: ArraySchemaType['options'] & TreeInputOptions;
|
|
75
|
+
}
|
|
76
|
+
export interface TreeDeskStructureProps extends TreeInputOptions {
|
|
77
|
+
/**
|
|
78
|
+
* _id of the document that will hold the tree data.
|
|
79
|
+
*/
|
|
80
|
+
documentId: string;
|
|
81
|
+
/**
|
|
82
|
+
* (Optional)
|
|
83
|
+
* Schema type for your hierarchical documents.
|
|
84
|
+
* By default, the document will be of type `hierarchy.tree`, a type provided by the plugin.
|
|
85
|
+
*/
|
|
86
|
+
documentType?: string;
|
|
87
|
+
/**
|
|
88
|
+
* (Optional)
|
|
89
|
+
* Key for the field representing the hierarchical tree inside the document.
|
|
90
|
+
* `tree` by default.
|
|
91
|
+
*/
|
|
92
|
+
fieldKeyInDocument?: string;
|
|
93
|
+
}
|
|
94
|
+
export interface DocumentPair {
|
|
95
|
+
draft?: SanityDocument;
|
|
96
|
+
published?: SanityDocument;
|
|
97
|
+
}
|
|
98
|
+
export interface AllItems {
|
|
99
|
+
[publishedId: string]: DocumentPair | undefined;
|
|
100
|
+
}
|
|
101
|
+
declare type DocumentOperation<Payload = unknown> = {
|
|
102
|
+
execute?: (payload?: Payload) => void;
|
|
103
|
+
disabled?: boolean | string;
|
|
104
|
+
};
|
|
105
|
+
export interface DocumentOperations {
|
|
106
|
+
patch?: DocumentOperation<unknown[]>;
|
|
107
|
+
commit?: DocumentOperation;
|
|
108
|
+
del?: DocumentOperation;
|
|
109
|
+
delete?: DocumentOperation;
|
|
110
|
+
discardChanges?: DocumentOperation;
|
|
111
|
+
duplicate?: DocumentOperation;
|
|
112
|
+
restore?: DocumentOperation;
|
|
113
|
+
unpublish?: DocumentOperation;
|
|
114
|
+
publish?: DocumentOperation;
|
|
115
|
+
}
|
|
116
|
+
export interface VisibilityMap {
|
|
117
|
+
[_key: string]: boolean;
|
|
118
|
+
}
|
|
119
|
+
export interface NodeProps extends NodeRendererProps {
|
|
120
|
+
node: LocalTreeItem;
|
|
121
|
+
}
|
|
122
|
+
export {};
|
package/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
interface TreeItemWithChildren extends
|
|
1
|
+
import { StoredTreeItem } from '../types';
|
|
2
|
+
interface TreeItemWithChildren extends StoredTreeItem {
|
|
3
3
|
children?: TreeItemWithChildren[];
|
|
4
4
|
}
|
|
5
|
-
export default function flatDataToTree(data:
|
|
5
|
+
export default function flatDataToTree(data: StoredTreeItem[]): TreeItemWithChildren[];
|
|
6
6
|
export {};
|
package/lib/utils/treeData.d.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { SanityDocument } from '@sanity/client';
|
|
2
2
|
import { TreeItem } from 'react-sortable-tree';
|
|
3
|
-
import { AllItems,
|
|
3
|
+
import { AllItems, EnhancedTreeItem, LocalTreeItem, StoredTreeItem, VisibilityMap } from '../types';
|
|
4
4
|
export declare const dataToEditorTree: ({ tree, allItems, visibilityMap }: {
|
|
5
|
-
tree:
|
|
5
|
+
tree: StoredTreeItem[];
|
|
6
6
|
allItems: AllItems;
|
|
7
7
|
visibilityMap: VisibilityMap;
|
|
8
|
-
}) =>
|
|
8
|
+
}) => LocalTreeItem[];
|
|
9
9
|
export declare const flatTree: (tree: TreeItem[]) => TreeItem[];
|
|
10
10
|
export interface FetchData {
|
|
11
|
-
mainTree?:
|
|
11
|
+
mainTree?: LocalTreeItem[];
|
|
12
12
|
allItems?: SanityDocument[];
|
|
13
13
|
}
|
|
14
14
|
export declare const getUnaddedItems: (data: {
|
|
15
15
|
allItems: AllItems;
|
|
16
|
-
tree:
|
|
17
|
-
}) =>
|
|
18
|
-
export declare function normalizeNodeForStorage(item:
|
|
16
|
+
tree: StoredTreeItem[];
|
|
17
|
+
}) => EnhancedTreeItem[];
|
|
18
|
+
export declare function normalizeNodeForStorage(item: LocalTreeItem): StoredTreeItem;
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { FullTree, NodeData,
|
|
2
|
-
import {
|
|
3
|
-
export declare type HandleMovedNodeData = Omit<NodeData & FullTree & OnMovePreviousAndNextLocation, 'prevPath' | 'prevTreeIndex' | 'path' | 'treeIndex'
|
|
1
|
+
import { FullTree, NodeData, OnMovePreviousAndNextLocation, TreeItem } from 'react-sortable-tree';
|
|
2
|
+
import { LocalTreeItem, NodeProps } from '../types';
|
|
3
|
+
export declare type HandleMovedNodeData = Omit<NodeData & FullTree & OnMovePreviousAndNextLocation, 'prevPath' | 'prevTreeIndex' | 'path' | 'treeIndex' | 'node'> & {
|
|
4
|
+
node: LocalTreeItem;
|
|
5
|
+
};
|
|
4
6
|
export declare type HandleMovedNode = (moveData: HandleMovedNodeData) => void;
|
|
5
|
-
export declare function getAddItemPatch(item:
|
|
6
|
-
export declare function getDuplicateItemPatch(nodeProps:
|
|
7
|
-
export declare function getRemoveItemPatch({ node }: Pick<
|
|
7
|
+
export declare function getAddItemPatch(item: LocalTreeItem): unknown[];
|
|
8
|
+
export declare function getDuplicateItemPatch(nodeProps: NodeProps): unknown[];
|
|
9
|
+
export declare function getRemoveItemPatch({ node }: Pick<NodeProps, 'node'>): unknown[];
|
|
8
10
|
export declare function getMovedNodePatch(data: HandleMovedNodeData): unknown[];
|
|
9
|
-
export declare function getMoveItemPatch({ nodeProps: { node, treeIndex
|
|
10
|
-
nodeProps:
|
|
11
|
+
export declare function getMoveItemPatch({ nodeProps: { node, treeIndex }, localTree, direction }: {
|
|
12
|
+
nodeProps: NodeProps;
|
|
11
13
|
localTree: TreeItem[];
|
|
12
14
|
direction: 'up' | 'down';
|
|
13
15
|
}): unknown[];
|
package/lib/utils/treePatches.js
CHANGED
|
@@ -61,9 +61,8 @@ export function getMovedNodePatch(data) {
|
|
|
61
61
|
? // After the sibling before it
|
|
62
62
|
Patch.insert([normalizedNode], 'after', [{ _key: leadingNode.node._key }])
|
|
63
63
|
: // Or before the sibling right after it, in case there's no leading sibling node
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
]),
|
|
64
|
+
// prettier-ignore
|
|
65
|
+
Patch.insert([normalizedNode], 'before', [followingNode?.node?._key ? { _key: followingNode.node._key } : data.nextTreeIndex]),
|
|
67
66
|
// 3. Patch the new node with its new `parent`
|
|
68
67
|
nextParentNode
|
|
69
68
|
? // If it has a parent node, set that parent's _key
|
|
@@ -80,7 +79,7 @@ function getChildrenPaths(node) {
|
|
|
80
79
|
.reduce((keyPaths, child) => [...keyPaths, child._key, ...getChildrenPaths(child)], [])
|
|
81
80
|
.filter(Boolean);
|
|
82
81
|
}
|
|
83
|
-
export function getMoveItemPatch({ nodeProps: { node, treeIndex
|
|
82
|
+
export function getMoveItemPatch({ nodeProps: { node, treeIndex }, localTree, direction = 'up' }) {
|
|
84
83
|
const keyPath = { _key: node._key };
|
|
85
84
|
const nextTreeIndex = treeIndex + (direction === 'up' ? -1 : 1);
|
|
86
85
|
const flatTree = getFlatDataFromTree({
|
|
@@ -98,16 +97,6 @@ export function getMoveItemPatch({ nodeProps: { node, treeIndex, parentNode }, l
|
|
|
98
97
|
treeIndex: nextTreeIndex
|
|
99
98
|
});
|
|
100
99
|
const normalizedNode = normalizeNodeForStorage(node);
|
|
101
|
-
console.log(`Move ${direction}`, {
|
|
102
|
-
node,
|
|
103
|
-
treeIndex,
|
|
104
|
-
parentNode,
|
|
105
|
-
nextFlatTree,
|
|
106
|
-
flatTree,
|
|
107
|
-
localTree,
|
|
108
|
-
leadingSibling: leadingNode,
|
|
109
|
-
followingSibling: followingNode
|
|
110
|
-
});
|
|
111
100
|
// When moving up, look at following node to figure out what is the next parent.
|
|
112
101
|
const nodeToInheritParent = direction === 'up' ? followingNode : leadingNode;
|
|
113
102
|
const nextParentNode = nodeToInheritParent?.parentNode;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { OnVisibilityToggleData
|
|
2
|
-
import { AllItems,
|
|
1
|
+
import { OnVisibilityToggleData } from 'react-sortable-tree';
|
|
2
|
+
import { AllItems, LocalTreeItem, StoredTreeItem } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* Enhances tree data with information on:
|
|
5
5
|
* - `expanded` - native property of react-sortable-tree to determine collapsing & expanding of a node's children
|
|
6
|
-
* - `draftId` & `publishedId` - refer to
|
|
6
|
+
* - `draftId` & `publishedId` - refer to LocalTreeItem's type annotations
|
|
7
7
|
*
|
|
8
8
|
* Doesn't modify the main tree or has side-effects on data.
|
|
9
9
|
* Has the added benefit of being local to the user, so external changes won't affect local visibility.
|
|
10
10
|
*/
|
|
11
11
|
export default function useLocalTree({ tree, allItems }: {
|
|
12
|
-
tree:
|
|
12
|
+
tree: StoredTreeItem[];
|
|
13
13
|
allItems: AllItems;
|
|
14
14
|
}): {
|
|
15
15
|
handleVisibilityToggle: (data: OnVisibilityToggleData) => void;
|
|
16
|
-
localTree:
|
|
16
|
+
localTree: LocalTreeItem[];
|
|
17
17
|
};
|
|
@@ -3,7 +3,7 @@ import { dataToEditorTree } from './treeData';
|
|
|
3
3
|
/**
|
|
4
4
|
* Enhances tree data with information on:
|
|
5
5
|
* - `expanded` - native property of react-sortable-tree to determine collapsing & expanding of a node's children
|
|
6
|
-
* - `draftId` & `publishedId` - refer to
|
|
6
|
+
* - `draftId` & `publishedId` - refer to LocalTreeItem's type annotations
|
|
7
7
|
*
|
|
8
8
|
* Doesn't modify the main tree or has side-effects on data.
|
|
9
9
|
* Has the added benefit of being local to the user, so external changes won't affect local visibility.
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { SanityTreeItem } from '../types';
|
|
1
|
+
import { LocalTreeItem, NodeProps } from '../types';
|
|
3
2
|
import { HandleMovedNode } from './treePatches';
|
|
4
3
|
export default function useTreeOperationsProvider(props: {
|
|
5
4
|
patchPrefix?: string;
|
|
6
5
|
onChange: (patch: unknown) => void;
|
|
7
|
-
localTree:
|
|
6
|
+
localTree: LocalTreeItem[];
|
|
8
7
|
}): {
|
|
9
8
|
handleMovedNode: HandleMovedNode;
|
|
10
|
-
addItem: (item:
|
|
11
|
-
duplicateItem: (nodeProps:
|
|
12
|
-
removeItem: (nodeProps:
|
|
13
|
-
moveItemUp: (nodeProps:
|
|
14
|
-
moveItemDown: (nodeProps:
|
|
9
|
+
addItem: (item: LocalTreeItem) => void;
|
|
10
|
+
duplicateItem: (nodeProps: NodeProps) => void;
|
|
11
|
+
removeItem: (nodeProps: NodeProps) => void;
|
|
12
|
+
moveItemUp: (nodeProps: NodeProps) => void;
|
|
13
|
+
moveItemDown: (nodeProps: NodeProps) => void;
|
|
15
14
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/hierarchical-document-list",
|
|
3
|
-
"version": "0.1.0-next.
|
|
3
|
+
"version": "0.1.0-next.2",
|
|
4
4
|
"author": "Sanity <hello@sanity.io>",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
"lint": "eslint src"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
+
"@sanity/base": ">= 2.25.0",
|
|
23
|
+
"@sanity/color": "^2.1.6",
|
|
24
|
+
"@sanity/desk-tool": ">= 2.25.0",
|
|
25
|
+
"@sanity/form-builder": "^2.25.0",
|
|
26
|
+
"@sanity/icons": ">= 1.2.0",
|
|
27
|
+
"@sanity/ui": ">= 0.37.0",
|
|
22
28
|
"@types/assert": "^1.5.6",
|
|
23
29
|
"@types/react": "^17.0.38",
|
|
24
30
|
"@types/react-dom": "^17.0.11",
|
|
@@ -30,25 +36,21 @@
|
|
|
30
36
|
"eslint-config-prettier": "^8.3.0",
|
|
31
37
|
"eslint-config-sanity": "^5.1.0",
|
|
32
38
|
"prettier": "^2.5.1",
|
|
39
|
+
"react": "^17.0.2",
|
|
40
|
+
"react-dom": "^17.0.2",
|
|
33
41
|
"styled-components": "^5.3.3",
|
|
34
42
|
"typescript": "^4.5.5"
|
|
35
43
|
},
|
|
36
44
|
"type": "module",
|
|
37
45
|
"dependencies": {
|
|
38
|
-
"@sanity/base": ">= 2.25.0",
|
|
39
|
-
"@sanity/color": "^2.1.6",
|
|
40
|
-
"@sanity/desk-tool": ">= 2.25.0",
|
|
41
|
-
"@sanity/form-builder": "^2.25.0",
|
|
42
|
-
"@sanity/icons": ">= 1.2.0",
|
|
43
|
-
"@sanity/ui": ">= 0.37.0",
|
|
44
46
|
"assert": "^2.0.0",
|
|
45
|
-
"react": "^17.0.2",
|
|
46
|
-
"react-dom": "^17.0.2",
|
|
47
47
|
"react-sortable-tree": "^2.8.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@sanity/base": ">= 2.25.0",
|
|
51
51
|
"@sanity/desk-tool": ">= 2.25.0",
|
|
52
|
+
"react": "^17.0.2",
|
|
53
|
+
"react-dom": "^17.0.2",
|
|
52
54
|
"styled-components": ">= 5.2.0"
|
|
53
55
|
}
|
|
54
56
|
}
|