@sanity/hierarchical-document-list 2.1.3 → 3.0.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/LICENSE +1 -1
- package/README.md +30 -28
- package/dist/index.d.ts +170 -195
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +835 -6031
- package/dist/index.js.map +1 -1
- package/package.json +38 -77
- package/dist/index.d.mts +0 -240
- package/dist/index.mjs +0 -6433
- package/dist/index.mjs.map +0 -1
- package/sanity.json +0 -8
- package/src/TreeDeskStructure.tsx +0 -80
- package/src/TreeInputComponent.tsx +0 -41
- package/src/components/DeskWarning.tsx +0 -40
- package/src/components/DocumentInNode.tsx +0 -133
- package/src/components/DocumentPreviewStatus.tsx +0 -70
- package/src/components/NodeActions.tsx +0 -85
- package/src/components/NodeContentRenderer.tsx +0 -141
- package/src/components/PlaceholderDropzone.tsx +0 -45
- package/src/components/TreeEditor.tsx +0 -184
- package/src/components/TreeEditorErrorBoundary.tsx +0 -14
- package/src/components/TreeNodeRenderer.tsx +0 -37
- package/src/components/TreeNodeRendererScaffold.tsx +0 -193
- package/src/createDeskHierarchy.tsx +0 -110
- package/src/createHierarchicalSchemas.tsx +0 -151
- package/src/hooks/useAllItems.ts +0 -119
- package/src/hooks/useLocalTree.ts +0 -40
- package/src/hooks/useTreeOperations.ts +0 -25
- package/src/hooks/useTreeOperationsProvider.ts +0 -86
- package/src/index.ts +0 -25
- package/src/schemas/hierarchy.tree.ts +0 -19
- package/src/types.ts +0 -148
- package/src/utils/flatDataToTree.ts +0 -20
- package/src/utils/getAdjescentNodes.ts +0 -30
- package/src/utils/getCommonTreeProps.tsx +0 -28
- package/src/utils/getTreeHeight.ts +0 -8
- package/src/utils/gradientPatchAdapter.ts +0 -43
- package/src/utils/idUtils.ts +0 -7
- package/src/utils/injectNodeTypeInPatches.ts +0 -60
- package/src/utils/moveItemInArray.ts +0 -26
- package/src/utils/throwError.ts +0 -9
- package/src/utils/treeData.tsx +0 -119
- package/src/utils/treePatches.ts +0 -171
- package/v2-incompatible.js +0 -11
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import SortableTree, {FullTree, NodeData} from '@nosferatu500/react-sortable-tree'
|
|
2
|
-
import {AddCircleIcon} from '@sanity/icons'
|
|
3
|
-
import {Box, Button, Card, Flex, Spinner, Stack, Text, Tooltip} from '@sanity/ui'
|
|
4
|
-
import * as React from 'react'
|
|
5
|
-
import {useCallback, useMemo} from 'react'
|
|
6
|
-
import {DndProvider} from 'react-dnd'
|
|
7
|
-
import {HTML5Backend} from 'react-dnd-html5-backend'
|
|
8
|
-
import {PatchEvent} from 'sanity'
|
|
9
|
-
import useAllItems from '../hooks/useAllItems'
|
|
10
|
-
import useLocalTree from '../hooks/useLocalTree'
|
|
11
|
-
import {TreeOperationsContext} from '../hooks/useTreeOperations'
|
|
12
|
-
import useTreeOperationsProvider from '../hooks/useTreeOperationsProvider'
|
|
13
|
-
import {Optional, StoredTreeItem, TreeDeskStructureProps} from '../types'
|
|
14
|
-
import getCommonTreeProps from '../utils/getCommonTreeProps'
|
|
15
|
-
import getTreeHeight from '../utils/getTreeHeight'
|
|
16
|
-
import {getUnaddedItems} from '../utils/treeData'
|
|
17
|
-
import {HandleMovedNodeData} from '../utils/treePatches'
|
|
18
|
-
import DocumentInNode from './DocumentInNode'
|
|
19
|
-
import {TreeEditorErrorBoundary} from './TreeEditorErrorBoundary'
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The loaded tree users interact with
|
|
23
|
-
*/
|
|
24
|
-
const TreeEditor: React.FC<{
|
|
25
|
-
tree: StoredTreeItem[]
|
|
26
|
-
onChange: (patch: PatchEvent) => void
|
|
27
|
-
options: Optional<TreeDeskStructureProps, 'documentId'>
|
|
28
|
-
patchPrefix?: string
|
|
29
|
-
}> = (props) => {
|
|
30
|
-
const {status: allItemsStatus, allItems} = useAllItems(props.options)
|
|
31
|
-
const unAddedItems = getUnaddedItems({tree: props.tree, allItems})
|
|
32
|
-
|
|
33
|
-
const {localTree, handleVisibilityToggle} = useLocalTree({
|
|
34
|
-
tree: props.tree,
|
|
35
|
-
allItems
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
const operations = useTreeOperationsProvider({
|
|
39
|
-
patchPrefix: props.patchPrefix,
|
|
40
|
-
onChange: props.onChange,
|
|
41
|
-
localTree
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const [context, setContext] = React.useState<HTMLElement | null>(null)
|
|
45
|
-
const [treeViewHeight, setTreeViewHeight] = React.useState<string>('')
|
|
46
|
-
|
|
47
|
-
const updateTreeViewHeight = () => {
|
|
48
|
-
const el = document.querySelector(`#${props.options.documentId} [data-known-size]`) as HTMLElement | null
|
|
49
|
-
const rowHeight = Number(el?.dataset.knownSize || 51)
|
|
50
|
-
setTreeViewHeight(getTreeHeight(localTree, rowHeight))
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
React.useEffect(() => {
|
|
54
|
-
if (props.options.documentId) {
|
|
55
|
-
setContext(document.getElementById(props.options.documentId))
|
|
56
|
-
}
|
|
57
|
-
}, [props.options.documentId])
|
|
58
|
-
|
|
59
|
-
React.useEffect(() => {
|
|
60
|
-
// Wait for dom to load before initial execution.
|
|
61
|
-
setTimeout(updateTreeViewHeight)
|
|
62
|
-
}, [])
|
|
63
|
-
|
|
64
|
-
React.useEffect(() => {
|
|
65
|
-
// Immediately update when changes are detected.
|
|
66
|
-
updateTreeViewHeight()
|
|
67
|
-
}, [props.options.documentId, localTree])
|
|
68
|
-
|
|
69
|
-
const onMoveNode = useCallback(
|
|
70
|
-
(data: NodeData & FullTree & any) =>
|
|
71
|
-
operations.handleMovedNode(data as unknown as HandleMovedNodeData),
|
|
72
|
-
[operations]
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
const treeProps = useMemo(
|
|
76
|
-
() =>
|
|
77
|
-
getCommonTreeProps({
|
|
78
|
-
placeholder: {
|
|
79
|
-
title: 'Add items from the list below'
|
|
80
|
-
}
|
|
81
|
-
}),
|
|
82
|
-
[]
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
const operationContext = useMemo(
|
|
86
|
-
() => ({...operations, allItemsStatus}),
|
|
87
|
-
[operations, allItemsStatus]
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<TreeEditorErrorBoundary>
|
|
92
|
-
{/*Use this Box-wrapper to get a context Element to prevent DndProvider to have to HTML% backend at the same time https://github.com/react-dnd/react-dnd/issues/186#issuecomment-978206387 */}
|
|
93
|
-
<Box id={props.options.documentId}>
|
|
94
|
-
{context ? (
|
|
95
|
-
<DndProvider backend={HTML5Backend} options={{rootElement: context}}>
|
|
96
|
-
<TreeOperationsContext.Provider value={operationContext}>
|
|
97
|
-
<Stack space={4} paddingTop={4}>
|
|
98
|
-
<Card
|
|
99
|
-
style={{minHeight: treeViewHeight}}
|
|
100
|
-
// Only include borderBottom if there's something to show in unadded items
|
|
101
|
-
borderBottom={allItemsStatus !== 'success' || unAddedItems?.length > 0}
|
|
102
|
-
>
|
|
103
|
-
<SortableTree
|
|
104
|
-
maxDepth={props.options.maxDepth}
|
|
105
|
-
onChange={doNothingOnChange}
|
|
106
|
-
onVisibilityToggle={handleVisibilityToggle}
|
|
107
|
-
canDrop={canDrop}
|
|
108
|
-
onMoveNode={onMoveNode}
|
|
109
|
-
treeData={localTree}
|
|
110
|
-
{...treeProps}
|
|
111
|
-
/>
|
|
112
|
-
</Card>
|
|
113
|
-
|
|
114
|
-
{allItemsStatus === 'success' && unAddedItems?.length > 0 && (
|
|
115
|
-
<Stack space={1} paddingX={2} paddingTop={3}>
|
|
116
|
-
<Stack space={2} paddingX={2} paddingBottom={3}>
|
|
117
|
-
<Text size={2} as="h2" weight="semibold">
|
|
118
|
-
Add more items
|
|
119
|
-
</Text>
|
|
120
|
-
<Text size={1} muted>
|
|
121
|
-
Only published documents are shown.
|
|
122
|
-
</Text>
|
|
123
|
-
</Stack>
|
|
124
|
-
{unAddedItems.map((item) => (
|
|
125
|
-
<DocumentInNode
|
|
126
|
-
key={item.publishedId || item.draftId}
|
|
127
|
-
item={item}
|
|
128
|
-
action={
|
|
129
|
-
<Tooltip
|
|
130
|
-
portal
|
|
131
|
-
placement="left"
|
|
132
|
-
content={
|
|
133
|
-
<Box padding={2}>
|
|
134
|
-
<Text size={1}>Add to list</Text>
|
|
135
|
-
</Box>
|
|
136
|
-
}
|
|
137
|
-
>
|
|
138
|
-
<Button
|
|
139
|
-
onClick={() => {
|
|
140
|
-
operations.addItem(item)
|
|
141
|
-
}}
|
|
142
|
-
mode="bleed"
|
|
143
|
-
icon={AddCircleIcon}
|
|
144
|
-
style={{cursor: 'pointer'}}
|
|
145
|
-
/>
|
|
146
|
-
</Tooltip>
|
|
147
|
-
}
|
|
148
|
-
/>
|
|
149
|
-
))}
|
|
150
|
-
</Stack>
|
|
151
|
-
)}
|
|
152
|
-
{allItemsStatus === 'loading' && (
|
|
153
|
-
<Flex padding={4} align={'center'} justify={'center'}>
|
|
154
|
-
<Spinner size={3} muted />
|
|
155
|
-
</Flex>
|
|
156
|
-
)}
|
|
157
|
-
{allItemsStatus === 'error' && (
|
|
158
|
-
<Flex padding={4} align={'center'} justify={'center'}>
|
|
159
|
-
<Text size={2} weight="semibold">
|
|
160
|
-
Something went wrong when loading documents
|
|
161
|
-
</Text>
|
|
162
|
-
</Flex>
|
|
163
|
-
)}
|
|
164
|
-
</Stack>
|
|
165
|
-
</TreeOperationsContext.Provider>
|
|
166
|
-
</DndProvider>
|
|
167
|
-
) : null}
|
|
168
|
-
</Box>
|
|
169
|
-
</TreeEditorErrorBoundary>
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function canDrop({nextPath, prevPath}: any & NodeData) {
|
|
174
|
-
const insideItself =
|
|
175
|
-
nextPath.length >= prevPath.length &&
|
|
176
|
-
prevPath.every((pathIndex: any, index: string | number) => nextPath[index] === pathIndex)
|
|
177
|
-
return !insideItself
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const doNothingOnChange = () => {
|
|
181
|
-
// Do nothing. onMoveNode will do all the work
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export default TreeEditor
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* react-sortable-tree emits a lot of random errors when dragging to invalid states,
|
|
3
|
-
* even when drag-targets are disabled.
|
|
4
|
-
*
|
|
5
|
-
* This boundary is a workaround so users are not pestered with error-toasts for things
|
|
6
|
-
* that have no functional impact.
|
|
7
|
-
*
|
|
8
|
-
* This boundry does NOT handle errors that happen in the React dnd
|
|
9
|
-
* event handlers, so there is addtional workarounds in the
|
|
10
|
-
* DnDManager.
|
|
11
|
-
* */
|
|
12
|
-
export const TreeEditorErrorBoundary = (props: any) => {
|
|
13
|
-
return props.children
|
|
14
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import TreeNodeRendererScaffold from './TreeNodeRendererScaffold'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* To prevent expand buttons from overflowing on the left, we add a minimum left padding to all entries
|
|
6
|
-
*/
|
|
7
|
-
const BASE_LEFT_PADDING = 10
|
|
8
|
-
const NESTING_PADDING = 14
|
|
9
|
-
|
|
10
|
-
const TreeNodeRenderer: any = (props: any) => {
|
|
11
|
-
const {children, lowerSiblingCounts, connectDropTarget, isOver, draggedNode, canDrop} = props
|
|
12
|
-
|
|
13
|
-
// Construct the scaffold representing the structure of the tree
|
|
14
|
-
const scaffoldBlockCount = lowerSiblingCounts.length
|
|
15
|
-
|
|
16
|
-
return connectDropTarget(
|
|
17
|
-
<div style={props.style}>
|
|
18
|
-
<div
|
|
19
|
-
style={{
|
|
20
|
-
// prettier-ignore
|
|
21
|
-
paddingLeft: `${BASE_LEFT_PADDING + (NESTING_PADDING * scaffoldBlockCount)}px`
|
|
22
|
-
}}
|
|
23
|
-
>
|
|
24
|
-
{React.Children.map(children, (child) =>
|
|
25
|
-
React.cloneElement(child as React.ReactElement<any>, {
|
|
26
|
-
isOver,
|
|
27
|
-
canDrop,
|
|
28
|
-
draggedNode
|
|
29
|
-
})
|
|
30
|
-
)}
|
|
31
|
-
</div>
|
|
32
|
-
<TreeNodeRendererScaffold {...props} />
|
|
33
|
-
</div>
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export default TreeNodeRenderer
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import {blue} from '@sanity/color'
|
|
2
|
-
import * as React from 'react'
|
|
3
|
-
import {createGlobalStyle} from 'styled-components'
|
|
4
|
-
|
|
5
|
-
// Adapted from react-sortable-tree/src/tree-node.js
|
|
6
|
-
const ScaffoldStyles = createGlobalStyle`
|
|
7
|
-
.rst__lineBlock,
|
|
8
|
-
.rst__absoluteLineBlock {
|
|
9
|
-
height: 100%;
|
|
10
|
-
position: relative;
|
|
11
|
-
display: inline-block;
|
|
12
|
-
--stroke-width: 3px;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.rst__absoluteLineBlock {
|
|
16
|
-
position: absolute;
|
|
17
|
-
top: 0;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/* Highlight line for pointing to dragged row destination
|
|
21
|
-
========================================================================== */
|
|
22
|
-
/**
|
|
23
|
-
* +--+--+
|
|
24
|
-
* | | |
|
|
25
|
-
* | | |
|
|
26
|
-
* | | |
|
|
27
|
-
* +--+--+
|
|
28
|
-
*/
|
|
29
|
-
.rst__highlightLineVertical {
|
|
30
|
-
z-index: 3;
|
|
31
|
-
}
|
|
32
|
-
.rst__highlightLineVertical::before {
|
|
33
|
-
position: absolute;
|
|
34
|
-
content: '';
|
|
35
|
-
background-color: ${blue[400].hex};
|
|
36
|
-
width: calc(var(--stroke-width) * 2);
|
|
37
|
-
margin-left: calc(var(--stroke-width) * -1);
|
|
38
|
-
left: 50%;
|
|
39
|
-
top: 0;
|
|
40
|
-
height: 100%;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
@keyframes arrow-pulse {
|
|
44
|
-
0% {
|
|
45
|
-
transform: translate(0, 0);
|
|
46
|
-
opacity: 0;
|
|
47
|
-
}
|
|
48
|
-
30% {
|
|
49
|
-
transform: translate(0, 300%);
|
|
50
|
-
opacity: 1;
|
|
51
|
-
}
|
|
52
|
-
70% {
|
|
53
|
-
transform: translate(0, 700%);
|
|
54
|
-
opacity: 1;
|
|
55
|
-
}
|
|
56
|
-
100% {
|
|
57
|
-
transform: translate(0, 1000%);
|
|
58
|
-
opacity: 0;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
.rst__highlightLineVertical::after {
|
|
62
|
-
content: '';
|
|
63
|
-
position: absolute;
|
|
64
|
-
height: 0;
|
|
65
|
-
margin-left: calc(var(--stroke-width) * -1);
|
|
66
|
-
left: 50%;
|
|
67
|
-
top: 0;
|
|
68
|
-
border-left: var(--stroke-width) solid transparent;
|
|
69
|
-
border-right: var(--stroke-width) solid transparent;
|
|
70
|
-
border-top: var(--stroke-width) solid white;
|
|
71
|
-
animation: arrow-pulse 1s infinite linear both;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* +-----+
|
|
76
|
-
* | |
|
|
77
|
-
* | +--+
|
|
78
|
-
* | | |
|
|
79
|
-
* +--+--+
|
|
80
|
-
*/
|
|
81
|
-
.rst__highlightTopLeftCorner::before {
|
|
82
|
-
z-index: 3;
|
|
83
|
-
content: '';
|
|
84
|
-
position: absolute;
|
|
85
|
-
border-top: solid calc(var(--stroke-width) * 2) ${blue[400].hex};
|
|
86
|
-
border-left: solid calc(var(--stroke-width) * 2) ${blue[400].hex};
|
|
87
|
-
box-sizing: border-box;
|
|
88
|
-
height: calc(50% + var(--stroke-width));
|
|
89
|
-
top: 50%;
|
|
90
|
-
margin-top: calc(var(--stroke-width) * -1);
|
|
91
|
-
right: 0;
|
|
92
|
-
width: calc(50% + var(--stroke-width));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* +--+--+
|
|
97
|
-
* | | |
|
|
98
|
-
* | | |
|
|
99
|
-
* | +->|
|
|
100
|
-
* +-----+
|
|
101
|
-
*/
|
|
102
|
-
.rst__highlightBottomLeftCorner {
|
|
103
|
-
z-index: 3;
|
|
104
|
-
}
|
|
105
|
-
.rst__highlightBottomLeftCorner::before {
|
|
106
|
-
content: '';
|
|
107
|
-
position: absolute;
|
|
108
|
-
border-bottom: solid calc(var(--stroke-width) * 2) ${blue[400].hex};
|
|
109
|
-
border-left: solid calc(var(--stroke-width) * 2) ${blue[400].hex};
|
|
110
|
-
box-sizing: border-box;
|
|
111
|
-
height: calc(100% + var(--stroke-width));
|
|
112
|
-
top: 0;
|
|
113
|
-
right: calc(var(--stroke-width) * 3);
|
|
114
|
-
width: calc(50% - calc(var(--stroke-width) * 2));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.rst__highlightBottomLeftCorner::after {
|
|
118
|
-
content: '';
|
|
119
|
-
position: absolute;
|
|
120
|
-
height: 0;
|
|
121
|
-
right: 0;
|
|
122
|
-
top: 100%;
|
|
123
|
-
margin-top: calc(var(--stroke-width) * -3);
|
|
124
|
-
border-top: calc(var(--stroke-width) * 3) solid transparent;
|
|
125
|
-
border-bottom: calc(var(--stroke-width) * 3) solid transparent;
|
|
126
|
-
border-left: calc(var(--stroke-width) * 3) solid ${blue[400].hex};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.rst__unclickable {
|
|
130
|
-
pointer-events: none;
|
|
131
|
-
margin-top: -calc(var(--stroke-width) * 3);
|
|
132
|
-
}
|
|
133
|
-
`
|
|
134
|
-
|
|
135
|
-
const TreeNodeRendererScaffold: React.FC<any> = (props) => {
|
|
136
|
-
const {
|
|
137
|
-
lowerSiblingCounts,
|
|
138
|
-
scaffoldBlockPxWidth,
|
|
139
|
-
listIndex,
|
|
140
|
-
swapDepth,
|
|
141
|
-
swapFrom,
|
|
142
|
-
swapLength,
|
|
143
|
-
treeIndex,
|
|
144
|
-
} = props
|
|
145
|
-
|
|
146
|
-
// Construct the scaffold representing the structure of the tree
|
|
147
|
-
const scaffold: React.ReactNode[] = lowerSiblingCounts.map(
|
|
148
|
-
(lowerSiblingCount: number, i: any) => {
|
|
149
|
-
if (lowerSiblingCount < 0 || treeIndex === listIndex || i !== swapDepth) {
|
|
150
|
-
return null
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// This row has been shifted, and is at the depth of
|
|
154
|
-
// the line pointing to the new destination
|
|
155
|
-
let highlightLineClass = ''
|
|
156
|
-
|
|
157
|
-
if (listIndex === (swapFrom || 0) + (swapLength || 0) - 1) {
|
|
158
|
-
// This block is on the bottom (target) line
|
|
159
|
-
// This block points at the target block (where the row will go when released)
|
|
160
|
-
highlightLineClass = 'rst__highlightBottomLeftCorner'
|
|
161
|
-
} else if (treeIndex === swapFrom) {
|
|
162
|
-
// This block is on the top (source) line
|
|
163
|
-
highlightLineClass = 'rst__highlightTopLeftCorner'
|
|
164
|
-
} else {
|
|
165
|
-
// This block is between the bottom and top
|
|
166
|
-
highlightLineClass = 'rst__highlightLineVertical'
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const style = {
|
|
170
|
-
width: scaffoldBlockPxWidth,
|
|
171
|
-
left: scaffoldBlockPxWidth * i,
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return (
|
|
175
|
-
<div
|
|
176
|
-
key={i}
|
|
177
|
-
style={style}
|
|
178
|
-
className={`rst__unclickable rst__absoluteLineBlock ${highlightLineClass || ''}`}
|
|
179
|
-
tabIndex={-1}
|
|
180
|
-
/>
|
|
181
|
-
)
|
|
182
|
-
}
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
return (
|
|
186
|
-
<>
|
|
187
|
-
{scaffold}
|
|
188
|
-
<ScaffoldStyles />
|
|
189
|
-
</>
|
|
190
|
-
)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export default TreeNodeRendererScaffold
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import {AddIcon} from '@sanity/icons'
|
|
2
|
-
import * as React from 'react'
|
|
3
|
-
import type {ConfigContext} from 'sanity'
|
|
4
|
-
import {StructureBuilder} from 'sanity/desk'
|
|
5
|
-
|
|
6
|
-
import TreeDeskStructure from './TreeDeskStructure'
|
|
7
|
-
import {TreeDeskStructureProps} from './types'
|
|
8
|
-
import throwError from './utils/throwError'
|
|
9
|
-
|
|
10
|
-
export interface TreeProps extends TreeDeskStructureProps {
|
|
11
|
-
/**
|
|
12
|
-
* Visible title above the tree.
|
|
13
|
-
* Also used as the label in the desk list item.
|
|
14
|
-
*/
|
|
15
|
-
title: string
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Optional icon for rendering the item in the desk structure.
|
|
19
|
-
*/
|
|
20
|
-
icon?: any
|
|
21
|
-
|
|
22
|
-
context?: ConfigContext | any
|
|
23
|
-
S?: StructureBuilder | any
|
|
24
|
-
/**
|
|
25
|
-
* Restrict document types that can be created.
|
|
26
|
-
*/
|
|
27
|
-
creatableTypes?: string[]
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const deskTreeValidator = (props: TreeProps): React.FC => {
|
|
31
|
-
const {documentId, referenceTo} = props
|
|
32
|
-
if (typeof documentId !== 'string' && !documentId) {
|
|
33
|
-
throwError('invalidDocumentId')
|
|
34
|
-
}
|
|
35
|
-
if (!Array.isArray(referenceTo)) {
|
|
36
|
-
throwError('invalidReferenceTo', `(documentId "${documentId}")`)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return (deskProps) => <TreeDeskStructure {...deskProps} options={props} />
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export default function createDeskHierarchy(props: TreeProps) {
|
|
43
|
-
const {documentId, referenceTo, referenceOptions, context, S, creatableTypes} = props
|
|
44
|
-
if (!S || !context) {
|
|
45
|
-
throw new Error('Invalid configuration. S or context props are undefined. ' +
|
|
46
|
-
'These props are available as function parameters when configuring structure, and must be passed along to createDeskHierarchy. ' +
|
|
47
|
-
'Confer the plugin README for example usage.')
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const {schema} = context
|
|
51
|
-
|
|
52
|
-
const safelyCreatableTypes =
|
|
53
|
-
creatableTypes && !creatableTypes.some((type) => referenceTo.indexOf(type))
|
|
54
|
-
? creatableTypes
|
|
55
|
-
: referenceTo
|
|
56
|
-
|
|
57
|
-
let mainList = (
|
|
58
|
-
referenceTo?.length === 1
|
|
59
|
-
? S.documentTypeList(referenceTo[0]).schemaType(referenceTo[0])
|
|
60
|
-
: S.documentList().filter('_type in $types').params({types: referenceTo})
|
|
61
|
-
)
|
|
62
|
-
.id(documentId)
|
|
63
|
-
.menuItems(
|
|
64
|
-
(safelyCreatableTypes || []).map((schemaType) =>
|
|
65
|
-
S.menuItem()
|
|
66
|
-
.intent({
|
|
67
|
-
type: 'create',
|
|
68
|
-
params: {type: schemaType}
|
|
69
|
-
})
|
|
70
|
-
.title(`Create ${schema.get(schemaType)?.title}`)
|
|
71
|
-
.icon(schema.get(schemaType)?.icon || AddIcon)
|
|
72
|
-
)
|
|
73
|
-
)
|
|
74
|
-
.canHandleIntent((intent: string, c: Record<string, unknown>) => {
|
|
75
|
-
// Can edit itself
|
|
76
|
-
if (intent === 'edit' && c.id === props.documentId) {
|
|
77
|
-
return true
|
|
78
|
-
}
|
|
79
|
-
// Can create & edit referenced document types
|
|
80
|
-
if (safelyCreatableTypes.includes(c.type as string)) {
|
|
81
|
-
return true
|
|
82
|
-
}
|
|
83
|
-
return false
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
if (referenceOptions?.filter) {
|
|
87
|
-
mainList = mainList.filter(referenceOptions.filter)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (referenceOptions?.filterParams) {
|
|
91
|
-
mainList = mainList.params(referenceOptions.filterParams)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return S.listItem()
|
|
95
|
-
.id(documentId)
|
|
96
|
-
.title(props.title || documentId)
|
|
97
|
-
.icon(props.icon)
|
|
98
|
-
.child(
|
|
99
|
-
Object.assign(
|
|
100
|
-
mainList.serialize(),
|
|
101
|
-
{
|
|
102
|
-
type: 'component',
|
|
103
|
-
component: deskTreeValidator(props),
|
|
104
|
-
options: props,
|
|
105
|
-
__preserveInstance: true
|
|
106
|
-
},
|
|
107
|
-
props.title ? {title: props.title} : {}
|
|
108
|
-
)
|
|
109
|
-
)
|
|
110
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import {ArraySchemaType} from 'sanity'
|
|
3
|
-
import {DEFAULT_FIELD_KEY} from './TreeDeskStructure'
|
|
4
|
-
import TreeInputComponent from './TreeInputComponent'
|
|
5
|
-
import {TreeDeskStructureProps, TreeFieldSchema} from './types'
|
|
6
|
-
import {
|
|
7
|
-
INTERNAL_NODE_ARRAY_TYPE,
|
|
8
|
-
INTERNAL_NODE_TYPE,
|
|
9
|
-
INTERNAL_NODE_VALUE_TYPE,
|
|
10
|
-
getSchemaTypeName
|
|
11
|
-
} from './utils/injectNodeTypeInPatches'
|
|
12
|
-
import throwError from './utils/throwError'
|
|
13
|
-
|
|
14
|
-
type SchemaOptions = Omit<TreeDeskStructureProps, 'documentId' | 'maxDepth'>
|
|
15
|
-
|
|
16
|
-
function createHierarchicalNodeValueType({
|
|
17
|
-
referenceTo,
|
|
18
|
-
referenceOptions,
|
|
19
|
-
documentType
|
|
20
|
-
}: SchemaOptions) {
|
|
21
|
-
return {
|
|
22
|
-
// when used inside the field, name & type are overwritten by createHierarchicalNodeType
|
|
23
|
-
name: documentType ? getSchemaTypeName(documentType, 'nodeValue') : INTERNAL_NODE_VALUE_TYPE,
|
|
24
|
-
type: 'object',
|
|
25
|
-
title: `Hierarchical node value (${documentType})`,
|
|
26
|
-
|
|
27
|
-
fields: [
|
|
28
|
-
{name: 'docType', type: 'string'},
|
|
29
|
-
{
|
|
30
|
-
name: 'reference',
|
|
31
|
-
type: 'reference',
|
|
32
|
-
weak: true,
|
|
33
|
-
to: referenceTo.map((type) => ({type})),
|
|
34
|
-
options: referenceOptions
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function createHierarchicalNodeType(options: SchemaOptions) {
|
|
41
|
-
return {
|
|
42
|
-
// name & type are overwritten by createHierarchicalField
|
|
43
|
-
name: options.documentType
|
|
44
|
-
? getSchemaTypeName(options.documentType, 'node')
|
|
45
|
-
: INTERNAL_NODE_TYPE,
|
|
46
|
-
title: `Hierarchical node (${options.documentType})`,
|
|
47
|
-
type: 'object',
|
|
48
|
-
fields: [
|
|
49
|
-
{name: 'parent', type: 'string'},
|
|
50
|
-
|
|
51
|
-
options.documentType
|
|
52
|
-
? {name: 'value', type: getSchemaTypeName(options.documentType, 'nodeValue')}
|
|
53
|
-
: // If no documentType is defined, use an anonymized inline object to avoid
|
|
54
|
-
// having to define another custom schema type through the plugin
|
|
55
|
-
{
|
|
56
|
-
...createHierarchicalNodeValueType(options),
|
|
57
|
-
name: 'value',
|
|
58
|
-
type: 'object'
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function createHierarchicalArrayType(options: SchemaOptions) {
|
|
65
|
-
return {
|
|
66
|
-
// name & type are overwritten by createHierarchicalField
|
|
67
|
-
name: options.documentType
|
|
68
|
-
? getSchemaTypeName(options.documentType, 'array')
|
|
69
|
-
: INTERNAL_NODE_ARRAY_TYPE,
|
|
70
|
-
title: `Hierarchical array of nodes (${options.documentType})`,
|
|
71
|
-
type: 'array',
|
|
72
|
-
of: [
|
|
73
|
-
options.documentType
|
|
74
|
-
? {type: getSchemaTypeName(options.documentType, 'node')}
|
|
75
|
-
: createHierarchicalNodeType(options)
|
|
76
|
-
]
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function createHierarchicalField({name, title, options, ...rest}: TreeFieldSchema): Omit<
|
|
81
|
-
ArraySchemaType,
|
|
82
|
-
'type' | 'jsonType' | 'of'
|
|
83
|
-
> & {
|
|
84
|
-
type: string
|
|
85
|
-
inputComponent: React.FC<any>
|
|
86
|
-
of?: any[]
|
|
87
|
-
} {
|
|
88
|
-
if (!Array.isArray(options?.referenceTo)) {
|
|
89
|
-
throwError('invalidReferenceTo', `(field of name "${name}")`)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
...rest,
|
|
94
|
-
name,
|
|
95
|
-
title,
|
|
96
|
-
inputComponent: TreeInputComponent,
|
|
97
|
-
options,
|
|
98
|
-
...(options.documentType
|
|
99
|
-
? {type: getSchemaTypeName(options.documentType, 'array')}
|
|
100
|
-
: {
|
|
101
|
-
...createHierarchicalArrayType(options),
|
|
102
|
-
name
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function createHierarchicalDocType(options: SchemaOptions) {
|
|
108
|
-
return {
|
|
109
|
-
name: options.documentType,
|
|
110
|
-
title: 'Hierarchical tree',
|
|
111
|
-
type: 'document',
|
|
112
|
-
// The plugin needs to define a `schemaType` with liveEdit enabled so that
|
|
113
|
-
// `useDocumentOperation` in TreeDeskStructure.tsx doesn't create drafts at every patch.
|
|
114
|
-
liveEdit: true,
|
|
115
|
-
fields: [
|
|
116
|
-
createHierarchicalField({
|
|
117
|
-
name: options.fieldKeyInDocument || DEFAULT_FIELD_KEY,
|
|
118
|
-
title: 'Hierarchical Tree',
|
|
119
|
-
options
|
|
120
|
-
})
|
|
121
|
-
],
|
|
122
|
-
preview: {
|
|
123
|
-
select: {
|
|
124
|
-
id: '_id',
|
|
125
|
-
tree: 'tree'
|
|
126
|
-
},
|
|
127
|
-
prepare({id, tree}: {id: string; tree: unknown[]}): Record<string, string> {
|
|
128
|
-
return {
|
|
129
|
-
title: `Hierarchical documents (ID: ${id})`,
|
|
130
|
-
subtitle: `${tree?.length || 0} document(s) in its list.`
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export default function createHierarchicalSchemas(options: SchemaOptions) {
|
|
138
|
-
if (!Array.isArray(options.referenceTo) || options.referenceTo.length <= 0) {
|
|
139
|
-
throwError('invalidReferenceTo')
|
|
140
|
-
}
|
|
141
|
-
if (!options.documentType) {
|
|
142
|
-
throwError('invalidDocumentType')
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return [
|
|
146
|
-
createHierarchicalDocType(options),
|
|
147
|
-
createHierarchicalArrayType(options),
|
|
148
|
-
createHierarchicalNodeType(options),
|
|
149
|
-
createHierarchicalNodeValueType(options)
|
|
150
|
-
]
|
|
151
|
-
}
|