@sanity/hierarchical-document-list 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +95 -59
- package/dist/index.d.ts +240 -0
- package/dist/index.esm.js +18 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -51
- package/sanity.json +2 -6
- package/src/TreeDeskStructure.tsx +80 -0
- package/src/TreeInputComponent.tsx +41 -0
- package/src/components/DeskWarning.tsx +40 -0
- package/src/components/DocumentInNode.tsx +133 -0
- package/src/components/DocumentPreviewStatus.tsx +70 -0
- package/src/components/NodeActions.tsx +85 -0
- package/src/components/NodeContentRenderer.tsx +141 -0
- package/src/components/PlaceholderDropzone.tsx +45 -0
- package/src/components/TreeEditor.tsx +167 -0
- package/{lib/components/TreeEditorErrorBoundary.d.ts → src/components/TreeEditorErrorBoundary.tsx} +2 -4
- package/src/components/TreeNodeRenderer.tsx +37 -0
- package/src/components/TreeNodeRendererScaffold.tsx +193 -0
- package/src/createDeskHierarchy.tsx +110 -0
- package/src/createHierarchicalSchemas.tsx +151 -0
- package/src/hooks/useAllItems.ts +119 -0
- package/src/hooks/useLocalTree.ts +40 -0
- package/src/hooks/useTreeOperations.ts +25 -0
- package/src/hooks/useTreeOperationsProvider.ts +86 -0
- package/src/index.ts +25 -0
- package/src/schemas/hierarchy.tree.ts +19 -0
- package/src/types.ts +148 -0
- package/src/utils/flatDataToTree.ts +20 -0
- package/src/utils/getAdjescentNodes.ts +30 -0
- package/src/utils/getCommonTreeProps.tsx +28 -0
- package/src/utils/getTreeHeight.ts +10 -0
- package/src/utils/gradientPatchAdapter.ts +43 -0
- package/src/utils/idUtils.ts +7 -0
- package/src/utils/injectNodeTypeInPatches.ts +60 -0
- package/src/utils/moveItemInArray.ts +26 -0
- package/src/utils/throwError.ts +9 -0
- package/src/utils/treeData.tsx +119 -0
- package/src/utils/treePatches.ts +171 -0
- package/v2-incompatible.js +11 -0
- package/.husky/commit-msg +0 -4
- package/.husky/pre-commit +0 -4
- package/.idea/hierarchical-document-list.iml +0 -11
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/prettier.xml +0 -7
- package/.idea/vcs.xml +0 -6
- package/CHANGELOG.md +0 -15
- package/commitlint.config.js +0 -3
- package/lib/TreeDeskStructure.d.ts +0 -8
- package/lib/TreeDeskStructure.js +0 -96
- package/lib/TreeInputComponent.d.ts +0 -19
- package/lib/TreeInputComponent.js +0 -50
- package/lib/components/DeskWarning.d.ts +0 -6
- package/lib/components/DeskWarning.js +0 -46
- package/lib/components/DocumentInNode.d.ts +0 -11
- package/lib/components/DocumentInNode.js +0 -81
- package/lib/components/DocumentPreviewStatus.d.ts +0 -7
- package/lib/components/DocumentPreviewStatus.js +0 -39
- package/lib/components/NodeActions.d.ts +0 -10
- package/lib/components/NodeActions.js +0 -61
- package/lib/components/NodeContentRenderer.d.ts +0 -8
- package/lib/components/NodeContentRenderer.js +0 -105
- package/lib/components/PlaceholderDropzone.d.ts +0 -9
- package/lib/components/PlaceholderDropzone.js +0 -30
- package/lib/components/SuppressedDnDManager.d.ts +0 -2
- package/lib/components/SuppressedDnDManager.js +0 -59
- package/lib/components/TreeEditor.d.ts +0 -12
- package/lib/components/TreeEditor.js +0 -74
- package/lib/components/TreeEditorErrorBoundary.js +0 -74
- package/lib/components/TreeNodeRenderer.d.ts +0 -3
- package/lib/components/TreeNodeRenderer.js +0 -59
- package/lib/components/TreeNodeRendererScaffold.d.ts +0 -4
- package/lib/components/TreeNodeRendererScaffold.js +0 -44
- package/lib/createDeskHierarchy.d.ts +0 -14
- package/lib/createDeskHierarchy.js +0 -84
- package/lib/createHierarchicalSchemas.d.ts +0 -98
- package/lib/createHierarchicalSchemas.js +0 -138
- package/lib/hooks/useAllItems.d.ts +0 -7
- package/lib/hooks/useAllItems.js +0 -119
- package/lib/hooks/useLocalTree.d.ts +0 -17
- package/lib/hooks/useLocalTree.js +0 -59
- package/lib/hooks/useTreeOperations.d.ts +0 -9
- package/lib/hooks/useTreeOperations.js +0 -39
- package/lib/hooks/useTreeOperationsProvider.d.ts +0 -14
- package/lib/hooks/useTreeOperationsProvider.js +0 -85
- package/lib/index.d.ts +0 -3
- package/lib/index.js +0 -12
- package/lib/schemas/hierarchy.tree.d.ts +0 -13
- package/lib/schemas/hierarchy.tree.js +0 -19
- package/lib/types.d.ts +0 -128
- package/lib/types.js +0 -2
- package/lib/utils/flatDataToTree.d.ts +0 -6
- package/lib/utils/flatDataToTree.js +0 -26
- package/lib/utils/getAdjescentNodes.d.ts +0 -12
- package/lib/utils/getAdjescentNodes.js +0 -19
- package/lib/utils/getCommonTreeProps.d.ts +0 -7
- package/lib/utils/getCommonTreeProps.js +0 -33
- package/lib/utils/getTreeHeight.d.ts +0 -3
- package/lib/utils/getTreeHeight.js +0 -11
- package/lib/utils/gradientPatchAdapter.d.ts +0 -4
- package/lib/utils/gradientPatchAdapter.js +0 -40
- package/lib/utils/idUtils.d.ts +0 -2
- package/lib/utils/idUtils.js +0 -13
- package/lib/utils/injectNodeTypeInPatches.d.ts +0 -12
- package/lib/utils/injectNodeTypeInPatches.js +0 -59
- package/lib/utils/moveItemInArray.d.ts +0 -5
- package/lib/utils/moveItemInArray.js +0 -26
- package/lib/utils/throwError.d.ts +0 -7
- package/lib/utils/throwError.js +0 -12
- package/lib/utils/treeData.d.ts +0 -18
- package/lib/utils/treeData.js +0 -118
- package/lib/utils/treePatches.d.ts +0 -15
- package/lib/utils/treePatches.js +0 -171
- package/lint-staged.config.js +0 -4
- package/screenshot-1.jpg +0 -0
- package/tsconfig.json +0 -20
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {isDescendant} from '@nosferatu500/react-sortable-tree'
|
|
2
|
+
import {cyan, gray, red} from '@sanity/color'
|
|
3
|
+
import {ChevronDownIcon, ChevronRightIcon, DragHandleIcon} from '@sanity/icons'
|
|
4
|
+
import {Box, Button, Flex, Spinner} from '@sanity/ui'
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import styled from 'styled-components'
|
|
7
|
+
|
|
8
|
+
const Root = styled.div`
|
|
9
|
+
// Adapted from react-sortable-tree/style.css
|
|
10
|
+
&[data-landing='true'] > *,
|
|
11
|
+
&[data-cancel='true'] > * {
|
|
12
|
+
opacity: 0 !important;
|
|
13
|
+
}
|
|
14
|
+
&[data-landing='true']::before,
|
|
15
|
+
&[data-cancel='true']::before {
|
|
16
|
+
background-color: ${cyan[50].hex};
|
|
17
|
+
border: 2px dashed ${gray[400].hex};
|
|
18
|
+
border-radius: 3px;
|
|
19
|
+
content: '';
|
|
20
|
+
position: absolute;
|
|
21
|
+
top: 0;
|
|
22
|
+
right: 0;
|
|
23
|
+
bottom: 0;
|
|
24
|
+
left: 0;
|
|
25
|
+
z-index: -1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
&[data-cancel='true']::before {
|
|
29
|
+
background-color: ${red[50].hex};
|
|
30
|
+
}
|
|
31
|
+
`
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Customization of react-sortable-tree's default node.
|
|
35
|
+
* Created in order to use Sanity UI for styles.
|
|
36
|
+
* Reference: https://github.com/frontend-collective/react-sortable-tree/blob/master/src/node-renderer-default.js
|
|
37
|
+
*/
|
|
38
|
+
const NodeContentRenderer: any = (props: any) => {
|
|
39
|
+
const {node, path, treeIndex, canDrag = false} = props
|
|
40
|
+
const nodeTitle = node.title
|
|
41
|
+
const Handle = React.useMemo(() => {
|
|
42
|
+
if (!canDrag) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
if (typeof node.children === 'function' && node.expanded) {
|
|
46
|
+
// Show a loading symbol on the handle when the children are expanded
|
|
47
|
+
// and yet still defined by a function (a callback to fetch the children)
|
|
48
|
+
return <Spinner />
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const BtnElement = (
|
|
52
|
+
<div>
|
|
53
|
+
<Button
|
|
54
|
+
mode="bleed"
|
|
55
|
+
paddingX={0}
|
|
56
|
+
paddingY={1}
|
|
57
|
+
style={{
|
|
58
|
+
cursor: node.publishedId ? 'grab' : 'default',
|
|
59
|
+
fontSize: '1.5625rem'
|
|
60
|
+
}}
|
|
61
|
+
data-ui="DragHandleButton"
|
|
62
|
+
data-drag-handle={canDrag}
|
|
63
|
+
disabled={!node.publishedId}
|
|
64
|
+
>
|
|
65
|
+
<DragHandleIcon style={{marginBottom: '-0.1em'}} />
|
|
66
|
+
</Button>
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// Don't allow editors to drag invalid documents
|
|
71
|
+
if (!node.publishedId) {
|
|
72
|
+
return BtnElement
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Show the handle used to initiate a drag-and-drop
|
|
76
|
+
return props.connectDragSource(BtnElement, {
|
|
77
|
+
dropEffect: 'copy'
|
|
78
|
+
})
|
|
79
|
+
}, [canDrag, node, typeof node.children === 'function'])
|
|
80
|
+
|
|
81
|
+
const isDraggedDescendant = props.draggedNode && isDescendant(props.draggedNode, node)
|
|
82
|
+
const isLandingPadActive = !props.didDrop && props.isDragging
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Box style={{position: 'relative'}}>
|
|
86
|
+
{props.toggleChildrenVisibility &&
|
|
87
|
+
node.children &&
|
|
88
|
+
(node.children.length > 0 || typeof node.children === 'function') && (
|
|
89
|
+
<div
|
|
90
|
+
style={{
|
|
91
|
+
position: 'absolute',
|
|
92
|
+
left: '-2px',
|
|
93
|
+
top: '40%',
|
|
94
|
+
transform: 'translate(-100%, -50%)'
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<Button
|
|
98
|
+
aria-label={node.expanded ? 'Collapse' : 'Expand'}
|
|
99
|
+
icon={
|
|
100
|
+
node.expanded ? (
|
|
101
|
+
<ChevronDownIcon color={gray[200].hex} />
|
|
102
|
+
) : (
|
|
103
|
+
<ChevronRightIcon color={gray[200].hex} />
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
mode="bleed"
|
|
107
|
+
fontSize={2}
|
|
108
|
+
padding={1}
|
|
109
|
+
type="button"
|
|
110
|
+
onClick={() =>
|
|
111
|
+
props.toggleChildrenVisibility?.({
|
|
112
|
+
node,
|
|
113
|
+
path,
|
|
114
|
+
treeIndex
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
{props.connectDragPreview(
|
|
122
|
+
<div>
|
|
123
|
+
<Root
|
|
124
|
+
data-landing={isLandingPadActive}
|
|
125
|
+
data-cancel={isLandingPadActive && !props.canDrop}
|
|
126
|
+
style={{
|
|
127
|
+
opacity: isDraggedDescendant ? 0.5 : 1
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
<Flex align="center">
|
|
131
|
+
{Handle}
|
|
132
|
+
{typeof nodeTitle === 'function' ? nodeTitle(props) : nodeTitle}
|
|
133
|
+
</Flex>
|
|
134
|
+
</Root>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</Box>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default NodeContentRenderer
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {Box, Card, CardTone, Stack, Text} from '@sanity/ui'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
|
|
4
|
+
const PlaceholderDropzone: React.FC<
|
|
5
|
+
{
|
|
6
|
+
tone?: CardTone
|
|
7
|
+
title: string
|
|
8
|
+
subtitle?: string
|
|
9
|
+
} & any
|
|
10
|
+
> = (props) => {
|
|
11
|
+
const isValid = props.isOver && props.canDrop
|
|
12
|
+
const isInvalid = props.isOver && !props.canDrop
|
|
13
|
+
let tone: CardTone = 'transparent'
|
|
14
|
+
if (isValid) {
|
|
15
|
+
tone = 'positive'
|
|
16
|
+
}
|
|
17
|
+
if (isInvalid) {
|
|
18
|
+
tone = 'caution'
|
|
19
|
+
}
|
|
20
|
+
return (
|
|
21
|
+
<Box padding={3}>
|
|
22
|
+
<Card
|
|
23
|
+
padding={5}
|
|
24
|
+
radius={2}
|
|
25
|
+
border
|
|
26
|
+
tone={tone}
|
|
27
|
+
style={{
|
|
28
|
+
borderStyle: props.isOver ? undefined : 'dashed',
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
<Stack space={2} style={{textAlign: 'center'}}>
|
|
32
|
+
<Text size={2} as="h2" muted>
|
|
33
|
+
{!props.isOver && props.title}
|
|
34
|
+
{isValid && 'Drop here'}
|
|
35
|
+
{isInvalid && 'Invalid location or element'}
|
|
36
|
+
</Text>
|
|
37
|
+
{props.subtitle && <Text size={1}>{props.subtitle}</Text>}
|
|
38
|
+
{props.children}
|
|
39
|
+
</Stack>
|
|
40
|
+
</Card>
|
|
41
|
+
</Box>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default PlaceholderDropzone
|
|
@@ -0,0 +1,167 @@
|
|
|
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
|
+
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
if (props.options.documentId) {
|
|
48
|
+
setContext(document.getElementById(props.options.documentId))
|
|
49
|
+
}
|
|
50
|
+
}, [props.options.documentId])
|
|
51
|
+
|
|
52
|
+
const onMoveNode = useCallback(
|
|
53
|
+
(data: NodeData & FullTree & any) =>
|
|
54
|
+
operations.handleMovedNode(data as unknown as HandleMovedNodeData),
|
|
55
|
+
[operations]
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const treeProps = useMemo(
|
|
59
|
+
() =>
|
|
60
|
+
getCommonTreeProps({
|
|
61
|
+
placeholder: {
|
|
62
|
+
title: 'Add items from the list below'
|
|
63
|
+
}
|
|
64
|
+
}),
|
|
65
|
+
[]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const operationContext = useMemo(
|
|
69
|
+
() => ({...operations, allItemsStatus}),
|
|
70
|
+
[operations, allItemsStatus]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<TreeEditorErrorBoundary>
|
|
75
|
+
{/*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 */}
|
|
76
|
+
<Box id={props.options.documentId}>
|
|
77
|
+
{context ? (
|
|
78
|
+
<DndProvider backend={HTML5Backend} options={{rootElement: context}}>
|
|
79
|
+
<TreeOperationsContext.Provider value={operationContext}>
|
|
80
|
+
<Stack space={4} paddingTop={4}>
|
|
81
|
+
<Card
|
|
82
|
+
style={{minHeight: getTreeHeight(localTree)}}
|
|
83
|
+
// Only include borderBottom if there's something to show in unadded items
|
|
84
|
+
borderBottom={allItemsStatus !== 'success' || unAddedItems?.length > 0}
|
|
85
|
+
>
|
|
86
|
+
<SortableTree
|
|
87
|
+
maxDepth={props.options.maxDepth}
|
|
88
|
+
onChange={doNothingOnChange}
|
|
89
|
+
onVisibilityToggle={handleVisibilityToggle}
|
|
90
|
+
canDrop={canDrop}
|
|
91
|
+
onMoveNode={onMoveNode}
|
|
92
|
+
treeData={localTree}
|
|
93
|
+
{...treeProps}
|
|
94
|
+
/>
|
|
95
|
+
</Card>
|
|
96
|
+
|
|
97
|
+
{allItemsStatus === 'success' && unAddedItems?.length > 0 && (
|
|
98
|
+
<Stack space={1} paddingX={2} paddingTop={3}>
|
|
99
|
+
<Stack space={2} paddingX={2} paddingBottom={3}>
|
|
100
|
+
<Text size={2} as="h2" weight="semibold">
|
|
101
|
+
Add more items
|
|
102
|
+
</Text>
|
|
103
|
+
<Text size={1} muted>
|
|
104
|
+
Only published documents are shown.
|
|
105
|
+
</Text>
|
|
106
|
+
</Stack>
|
|
107
|
+
{unAddedItems.map((item) => (
|
|
108
|
+
<DocumentInNode
|
|
109
|
+
key={item.publishedId || item.draftId}
|
|
110
|
+
item={item}
|
|
111
|
+
action={
|
|
112
|
+
<Tooltip
|
|
113
|
+
portal
|
|
114
|
+
placement="left"
|
|
115
|
+
content={
|
|
116
|
+
<Box padding={2}>
|
|
117
|
+
<Text size={1}>Add to list</Text>
|
|
118
|
+
</Box>
|
|
119
|
+
}
|
|
120
|
+
>
|
|
121
|
+
<Button
|
|
122
|
+
onClick={() => {
|
|
123
|
+
operations.addItem(item)
|
|
124
|
+
}}
|
|
125
|
+
mode="bleed"
|
|
126
|
+
icon={AddCircleIcon}
|
|
127
|
+
style={{cursor: 'pointer'}}
|
|
128
|
+
/>
|
|
129
|
+
</Tooltip>
|
|
130
|
+
}
|
|
131
|
+
/>
|
|
132
|
+
))}
|
|
133
|
+
</Stack>
|
|
134
|
+
)}
|
|
135
|
+
{allItemsStatus === 'loading' && (
|
|
136
|
+
<Flex padding={4} align={'center'} justify={'center'}>
|
|
137
|
+
<Spinner size={3} muted />
|
|
138
|
+
</Flex>
|
|
139
|
+
)}
|
|
140
|
+
{allItemsStatus === 'error' && (
|
|
141
|
+
<Flex padding={4} align={'center'} justify={'center'}>
|
|
142
|
+
<Text size={2} weight="semibold">
|
|
143
|
+
Something went wrong when loading documents
|
|
144
|
+
</Text>
|
|
145
|
+
</Flex>
|
|
146
|
+
)}
|
|
147
|
+
</Stack>
|
|
148
|
+
</TreeOperationsContext.Provider>
|
|
149
|
+
</DndProvider>
|
|
150
|
+
) : null}
|
|
151
|
+
</Box>
|
|
152
|
+
</TreeEditorErrorBoundary>
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function canDrop({nextPath, prevPath}: any & NodeData) {
|
|
157
|
+
const insideItself =
|
|
158
|
+
nextPath.length >= prevPath.length &&
|
|
159
|
+
prevPath.every((pathIndex: any, index: string | number) => nextPath[index] === pathIndex)
|
|
160
|
+
return !insideItself
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const doNothingOnChange = () => {
|
|
164
|
+
// Do nothing. onMoveNode will do all the work
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export default TreeEditor
|
package/{lib/components/TreeEditorErrorBoundary.d.ts → src/components/TreeEditorErrorBoundary.tsx}
RENAMED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
1
|
/**
|
|
3
2
|
* react-sortable-tree emits a lot of random errors when dragging to invalid states,
|
|
4
3
|
* even when drag-targets are disabled.
|
|
@@ -10,7 +9,6 @@ import * as React from 'react';
|
|
|
10
9
|
* event handlers, so there is addtional workarounds in the
|
|
11
10
|
* DnDManager.
|
|
12
11
|
* */
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
render(): React.ReactNode;
|
|
12
|
+
export const TreeEditorErrorBoundary = (props: any) => {
|
|
13
|
+
return props.children
|
|
16
14
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
|
@@ -0,0 +1,193 @@
|
|
|
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
|