@sanity/hierarchical-document-list 1.1.0 → 2.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 +95 -59
- package/dist/index.d.ts +240 -0
- package/dist/index.esm.js +151 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -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
package/src/types.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {TreeItem} from '@nosferatu500/react-sortable-tree'
|
|
2
|
+
import {ArraySchemaType, ObjectSchemaType, SanityDocument} from 'sanity'
|
|
3
|
+
import {INTERNAL_NODE_TYPE, INTERNAL_NODE_VALUE_TYPE} from './utils/injectNodeTypeInPatches'
|
|
4
|
+
|
|
5
|
+
interface SanityReference {
|
|
6
|
+
_type: 'reference'
|
|
7
|
+
_ref: string
|
|
8
|
+
_weak?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Objects saved to tree documents in Sanity's Content Lake
|
|
13
|
+
*/
|
|
14
|
+
export interface StoredTreeItem {
|
|
15
|
+
_key: string
|
|
16
|
+
_type: typeof INTERNAL_NODE_TYPE | string
|
|
17
|
+
value?: {
|
|
18
|
+
_type: typeof INTERNAL_NODE_VALUE_TYPE | string
|
|
19
|
+
reference?: SanityReference
|
|
20
|
+
docType?: string
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* _key of parent node
|
|
24
|
+
*/
|
|
25
|
+
parent?: string | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Tree items enhanced locally in the client with info from `allItems` and `visibilityMap`.
|
|
30
|
+
* `allItems` stop here and never become LocalTreeItems as they aren't added to react-sortable-tree.
|
|
31
|
+
*
|
|
32
|
+
* See `useLocalTree.ts` and `dataToEditorTree()`.
|
|
33
|
+
*/
|
|
34
|
+
export interface EnhancedTreeItem extends StoredTreeItem {
|
|
35
|
+
expanded?: boolean | undefined
|
|
36
|
+
/**
|
|
37
|
+
* Used by DocumentInNode to render the preview for drafts if they exist.
|
|
38
|
+
* Also informs document status icons.
|
|
39
|
+
*/
|
|
40
|
+
draftId?: string
|
|
41
|
+
draftUpdatedAt?: string
|
|
42
|
+
/**
|
|
43
|
+
* If not present, DocumentInNode will show up an error for invalid document.
|
|
44
|
+
* - undefined `publishedId` could mean the document is either deleted, or it doesn't match GROQ filters anymore
|
|
45
|
+
*/
|
|
46
|
+
publishedId?: string
|
|
47
|
+
publishedUpdatedAt?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Tree items as found in the sortable tree itself.
|
|
52
|
+
*/
|
|
53
|
+
export type LocalTreeItem = EnhancedTreeItem & Pick<TreeItem, 'title' | 'children'>
|
|
54
|
+
|
|
55
|
+
export interface TreeInputOptions {
|
|
56
|
+
/**
|
|
57
|
+
* What document types this hierarchy can refer to.
|
|
58
|
+
* Similar to the `to` property of the [reference field](https://www.sanity.io/docs/reference-type).
|
|
59
|
+
*/
|
|
60
|
+
referenceTo: string[]
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Used to provide fine-grained filtering for documents.
|
|
64
|
+
*/
|
|
65
|
+
referenceOptions?: {
|
|
66
|
+
/**
|
|
67
|
+
* Static filter to apply to tree document queries.
|
|
68
|
+
*/
|
|
69
|
+
filter?: string
|
|
70
|
+
/**
|
|
71
|
+
* Parameters / variables to pass to the GROQ query ran to fetch documents.
|
|
72
|
+
*/
|
|
73
|
+
filterParams?: Record<string, unknown>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* How deep should editors be allowed to nest items.
|
|
78
|
+
*/
|
|
79
|
+
maxDepth?: number
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Schema type for your hierarchical documents.
|
|
83
|
+
* Refer to documentation on how to provide these schemas in your studio.
|
|
84
|
+
*
|
|
85
|
+
* Defautlt: 'hierarchy.tree' - this schema is bundled with the plugin
|
|
86
|
+
*/
|
|
87
|
+
documentType?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface TreeFieldSchema
|
|
91
|
+
extends Omit<ArraySchemaType, 'of' | 'type' | 'inputComponent' | 'jsonType'> {
|
|
92
|
+
options: ArraySchemaType['options'] & TreeInputOptions
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface TreeNodeObjectSchema
|
|
96
|
+
extends Omit<ObjectSchemaType, 'name' | 'fields' | 'type' | 'inputComponent' | 'jsonType'> {
|
|
97
|
+
options: ObjectSchemaType['options'] & TreeInputOptions
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface TreeDeskStructureProps extends TreeInputOptions {
|
|
101
|
+
/**
|
|
102
|
+
* _id of the document that will hold the tree data.
|
|
103
|
+
*/
|
|
104
|
+
documentId: string
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* (Optional)
|
|
108
|
+
* Key for the field representing the hierarchical tree inside the document.
|
|
109
|
+
* `tree` by default.
|
|
110
|
+
*/
|
|
111
|
+
fieldKeyInDocument?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface DocumentPair {
|
|
115
|
+
draft?: SanityDocument
|
|
116
|
+
published?: SanityDocument
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface AllItems {
|
|
120
|
+
[publishedId: string]: DocumentPair | undefined
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
type DocumentOperation<Payload = unknown> = {
|
|
124
|
+
execute?: (payload?: Payload) => void
|
|
125
|
+
disabled?: boolean | string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface DocumentOperations {
|
|
129
|
+
patch?: DocumentOperation<unknown[]>
|
|
130
|
+
commit?: DocumentOperation
|
|
131
|
+
del?: DocumentOperation
|
|
132
|
+
delete?: DocumentOperation
|
|
133
|
+
discardChanges?: DocumentOperation
|
|
134
|
+
duplicate?: DocumentOperation
|
|
135
|
+
restore?: DocumentOperation
|
|
136
|
+
unpublish?: DocumentOperation
|
|
137
|
+
publish?: DocumentOperation
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface VisibilityMap {
|
|
141
|
+
[_key: string]: boolean
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface NodeProps {
|
|
145
|
+
node: LocalTreeItem
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {getTreeFromFlatData} from '@nosferatu500/react-sortable-tree'
|
|
2
|
+
import {StoredTreeItem} from '../types'
|
|
3
|
+
|
|
4
|
+
interface TreeItemWithChildren extends StoredTreeItem {
|
|
5
|
+
children?: TreeItemWithChildren[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function flatDataToTree(data: StoredTreeItem[]): TreeItemWithChildren[] {
|
|
9
|
+
return getTreeFromFlatData({
|
|
10
|
+
flatData: data.map((item) => ({
|
|
11
|
+
...item,
|
|
12
|
+
// if parent: undefined, the tree won't be constructed
|
|
13
|
+
parent: item.parent || null,
|
|
14
|
+
})),
|
|
15
|
+
getKey: (item) => item._key,
|
|
16
|
+
getParentKey: (item) => item.parent,
|
|
17
|
+
// without rootKey, the tree won't be constructed
|
|
18
|
+
rootKey: null as any,
|
|
19
|
+
}) as TreeItemWithChildren[]
|
|
20
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import {FlatDataItem, TreeItem} from '@nosferatu500/react-sortable-tree'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gets adjescent non-children nodes of a given treeIndex.
|
|
5
|
+
*/
|
|
6
|
+
export default function getAdjescentNodes({
|
|
7
|
+
flatTree,
|
|
8
|
+
node,
|
|
9
|
+
treeIndex,
|
|
10
|
+
}: {
|
|
11
|
+
flatTree: FlatDataItem[]
|
|
12
|
+
node: TreeItem
|
|
13
|
+
treeIndex: number
|
|
14
|
+
}): {
|
|
15
|
+
leadingNode?: FlatDataItem
|
|
16
|
+
followingNode?: FlatDataItem
|
|
17
|
+
} {
|
|
18
|
+
const leadingNode = flatTree
|
|
19
|
+
.slice(0, treeIndex)
|
|
20
|
+
.reverse()
|
|
21
|
+
// Disregard children nodes - these include the current node's key in their `path` array
|
|
22
|
+
.find((item) => !item.path.includes(node._key))
|
|
23
|
+
|
|
24
|
+
const followingNode = flatTree.slice(treeIndex + 1).find((item) => !item.path.includes(node._key))
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
leadingNode,
|
|
28
|
+
followingNode,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {ReactSortableTreeProps} from '@nosferatu500/react-sortable-tree/react-sortable-tree'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import NodeContentRenderer from '../components/NodeContentRenderer'
|
|
4
|
+
import PlaceholderDropzone from '../components/PlaceholderDropzone'
|
|
5
|
+
import TreeNodeRenderer from '../components/TreeNodeRenderer'
|
|
6
|
+
// import {ROW_HEIGHT} from './getTreeHeight'
|
|
7
|
+
|
|
8
|
+
export default function getCommonTreeProps({
|
|
9
|
+
placeholder
|
|
10
|
+
}: {
|
|
11
|
+
placeholder: {
|
|
12
|
+
title: string
|
|
13
|
+
subtitle?: string
|
|
14
|
+
}
|
|
15
|
+
}): Partial<ReactSortableTreeProps> {
|
|
16
|
+
return {
|
|
17
|
+
theme: {
|
|
18
|
+
nodeContentRenderer: NodeContentRenderer,
|
|
19
|
+
placeholderRenderer: (props: any) => <PlaceholderDropzone {...placeholder} {...props} />,
|
|
20
|
+
treeNodeRenderer: TreeNodeRenderer,
|
|
21
|
+
style: {height: '100%'},
|
|
22
|
+
innerStyle: undefined,
|
|
23
|
+
scaffoldBlockPxWidth: 44,
|
|
24
|
+
slideRegionSize: 100
|
|
25
|
+
// rowHeight: ROW_HEIGHT,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {getVisibleNodeCount, TreeItem} from '@nosferatu500/react-sortable-tree'
|
|
2
|
+
|
|
3
|
+
export const ROW_HEIGHT = 51
|
|
4
|
+
|
|
5
|
+
export default function getTreeHeight(treeData: TreeItem[]): string {
|
|
6
|
+
const visibleNodeCount = getVisibleNodeCount({treeData})
|
|
7
|
+
|
|
8
|
+
// prettier-ignore
|
|
9
|
+
return `${50 + (ROW_HEIGHT * visibleNodeCount)}px`
|
|
10
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Adapted from @sanity/form-builder/src/sanity/utils/gradientPatchAdapter.ts
|
|
2
|
+
import {arrayToJSONMatchPath} from '@sanity/mutator'
|
|
3
|
+
|
|
4
|
+
type Patch = Record<string, any>
|
|
5
|
+
|
|
6
|
+
type GradientPatch = Record<string, any>
|
|
7
|
+
|
|
8
|
+
export function toGradient(patches: Patch[]): GradientPatch[] {
|
|
9
|
+
return patches.map(toGradientPatch)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toGradientPatch(patch: Patch): GradientPatch {
|
|
13
|
+
const matchPath = arrayToJSONMatchPath(patch.path || [])
|
|
14
|
+
if (patch.type === 'insert') {
|
|
15
|
+
const {position, items} = patch
|
|
16
|
+
return {
|
|
17
|
+
insert: {
|
|
18
|
+
[position]: matchPath,
|
|
19
|
+
items: items,
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (patch.type === 'unset') {
|
|
25
|
+
return {
|
|
26
|
+
unset: [matchPath],
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!patch.type) {
|
|
31
|
+
throw new Error(`Missing patch type in patch ${JSON.stringify(patch)}`)
|
|
32
|
+
}
|
|
33
|
+
if (matchPath) {
|
|
34
|
+
return {
|
|
35
|
+
[patch.type]: {
|
|
36
|
+
[matchPath]: patch.value,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
[patch.type]: patch.value,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export function getSchemaTypeName(
|
|
2
|
+
documentType: string,
|
|
3
|
+
type: 'document' | 'node' | 'nodeValue' | 'array'
|
|
4
|
+
): string {
|
|
5
|
+
switch (type) {
|
|
6
|
+
case 'document':
|
|
7
|
+
return documentType
|
|
8
|
+
case 'array':
|
|
9
|
+
return `${documentType}.array`
|
|
10
|
+
case 'node':
|
|
11
|
+
return `${documentType}.node`
|
|
12
|
+
case 'nodeValue':
|
|
13
|
+
return `${documentType}.nodeValue`
|
|
14
|
+
default:
|
|
15
|
+
return documentType
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Temporary value of nodes' `_type` before they are normalized and persisted as the user-defined choice.
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_DOC_TYPE = 'hierarchy.tree'
|
|
23
|
+
export const INTERNAL_NODE_TYPE = getSchemaTypeName(DEFAULT_DOC_TYPE, 'node')
|
|
24
|
+
export const INTERNAL_NODE_VALUE_TYPE = getSchemaTypeName(DEFAULT_DOC_TYPE, 'nodeValue')
|
|
25
|
+
export const INTERNAL_NODE_ARRAY_TYPE = getSchemaTypeName(DEFAULT_DOC_TYPE, 'array')
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Barebones recursive utility to inject the desired nodeObjectType in patches generated in deeply nested components and utilities.
|
|
29
|
+
*/
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
31
|
+
export default function injectNodeTypeInPatches(patchData: any, documentType: string): any {
|
|
32
|
+
if (Array.isArray(patchData)) {
|
|
33
|
+
return patchData.map((child) => injectNodeTypeInPatches(child, documentType))
|
|
34
|
+
}
|
|
35
|
+
if (typeof patchData === 'object' && patchData !== null) {
|
|
36
|
+
return Object.keys(patchData).reduce((newObject, key) => {
|
|
37
|
+
const value = patchData[key as keyof typeof patchData]
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
key === '_type' &&
|
|
41
|
+
typeof value === 'string' &&
|
|
42
|
+
[INTERNAL_NODE_TYPE, INTERNAL_NODE_VALUE_TYPE].includes(value)
|
|
43
|
+
) {
|
|
44
|
+
return {
|
|
45
|
+
...newObject,
|
|
46
|
+
[key]: getSchemaTypeName(
|
|
47
|
+
documentType,
|
|
48
|
+
value === INTERNAL_NODE_TYPE ? 'node' : 'nodeValue'
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...newObject,
|
|
55
|
+
[key]: injectNodeTypeInPatches(value, documentType),
|
|
56
|
+
}
|
|
57
|
+
}, {})
|
|
58
|
+
}
|
|
59
|
+
return patchData
|
|
60
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export default function moveItemInArray<ItemType = unknown>({
|
|
2
|
+
array,
|
|
3
|
+
fromIndex,
|
|
4
|
+
toIndex,
|
|
5
|
+
}: {
|
|
6
|
+
array: ItemType[]
|
|
7
|
+
fromIndex: number
|
|
8
|
+
toIndex: number
|
|
9
|
+
}): ItemType[] {
|
|
10
|
+
if (fromIndex === toIndex) {
|
|
11
|
+
return array
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const newArray = [...array]
|
|
15
|
+
|
|
16
|
+
const target = newArray[fromIndex]
|
|
17
|
+
const inc = toIndex < fromIndex ? -1 : 1
|
|
18
|
+
|
|
19
|
+
for (let i = fromIndex; i !== toIndex; i += inc) {
|
|
20
|
+
newArray[i] = newArray[i + inc]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
newArray[toIndex] = target
|
|
24
|
+
|
|
25
|
+
return newArray
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const ERROR_MESSAGES = {
|
|
2
|
+
invalidDocumentId: 'Please add a documentId to your tree',
|
|
3
|
+
invalidDocumentType: 'Please add a valid documentType to createHierarchicalSchemas',
|
|
4
|
+
invalidReferenceTo: "Missing valid 'referenceTo' value",
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function throwError(message: keyof typeof ERROR_MESSAGES, extraContext = ''): void {
|
|
8
|
+
throw new Error(`[hierarchical input] ${ERROR_MESSAGES[message]} ${extraContext}`)
|
|
9
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import {TreeItem} from '@nosferatu500/react-sortable-tree'
|
|
2
|
+
import {randomKey} from '@sanity/util/content'
|
|
3
|
+
import {SanityDocument} from 'sanity'
|
|
4
|
+
import DocumentInNode from '../components/DocumentInNode'
|
|
5
|
+
import NodeActions from '../components/NodeActions'
|
|
6
|
+
import {
|
|
7
|
+
AllItems,
|
|
8
|
+
DocumentPair,
|
|
9
|
+
EnhancedTreeItem,
|
|
10
|
+
LocalTreeItem,
|
|
11
|
+
NodeProps,
|
|
12
|
+
StoredTreeItem,
|
|
13
|
+
VisibilityMap
|
|
14
|
+
} from '../types'
|
|
15
|
+
import flatDataToTree from './flatDataToTree'
|
|
16
|
+
import {INTERNAL_NODE_TYPE, INTERNAL_NODE_VALUE_TYPE} from './injectNodeTypeInPatches'
|
|
17
|
+
|
|
18
|
+
export const dataToEditorTree = ({
|
|
19
|
+
tree,
|
|
20
|
+
allItems,
|
|
21
|
+
visibilityMap
|
|
22
|
+
}: {
|
|
23
|
+
tree: StoredTreeItem[]
|
|
24
|
+
allItems: AllItems
|
|
25
|
+
visibilityMap: VisibilityMap
|
|
26
|
+
}): LocalTreeItem[] => {
|
|
27
|
+
const itemsWithTitle = tree
|
|
28
|
+
.filter((item) => item?.value?.reference?._ref)
|
|
29
|
+
.map((item) => {
|
|
30
|
+
const refId = item.value?.reference?._ref
|
|
31
|
+
const docPair = refId ? allItems[refId] : undefined
|
|
32
|
+
const draftDoc = docPair?.draft
|
|
33
|
+
const publishedDoc = docPair?.published
|
|
34
|
+
|
|
35
|
+
const enhancedItem: LocalTreeItem = {
|
|
36
|
+
...item,
|
|
37
|
+
expanded: visibilityMap[item._key] !== false,
|
|
38
|
+
draftId: draftDoc?._id,
|
|
39
|
+
publishedId: publishedDoc?._id,
|
|
40
|
+
draftUpdatedAt: draftDoc?._updatedAt,
|
|
41
|
+
publishedUpdatedAt: publishedDoc?._updatedAt
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...enhancedItem,
|
|
46
|
+
title: (nodeProps: NodeProps) => (
|
|
47
|
+
<DocumentInNode item={enhancedItem} action={<NodeActions nodeProps={nodeProps} />} />
|
|
48
|
+
),
|
|
49
|
+
children: []
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
return flatDataToTree(itemsWithTitle)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const documentPairToNode = (doc?: DocumentPair): EnhancedTreeItem | undefined => {
|
|
56
|
+
if (!doc?.published?._id) {
|
|
57
|
+
return undefined
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
_key: randomKey(12),
|
|
62
|
+
_type: INTERNAL_NODE_TYPE,
|
|
63
|
+
draftId: doc.draft?._id,
|
|
64
|
+
draftUpdatedAt: doc.draft?._updatedAt,
|
|
65
|
+
publishedId: doc.published._id,
|
|
66
|
+
publishedUpdatedAt: doc.published?._updatedAt,
|
|
67
|
+
value: {
|
|
68
|
+
_type: INTERNAL_NODE_VALUE_TYPE,
|
|
69
|
+
reference: {
|
|
70
|
+
_ref: doc.published._id,
|
|
71
|
+
_type: 'reference',
|
|
72
|
+
_weak: true
|
|
73
|
+
},
|
|
74
|
+
docType: doc.published._type
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const flatTree = (tree: TreeItem[]): TreeItem[] => {
|
|
80
|
+
return tree.reduce((flattened, item) => {
|
|
81
|
+
const {children, ...node} = item
|
|
82
|
+
return [...flattened, node, ...(Array.isArray(children) ? flatTree(children) : [])]
|
|
83
|
+
}, [] as TreeItem[])
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface FetchData {
|
|
87
|
+
mainTree?: LocalTreeItem[]
|
|
88
|
+
allItems?: SanityDocument[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const getUnaddedItems = (data: {
|
|
92
|
+
allItems: AllItems
|
|
93
|
+
tree: StoredTreeItem[]
|
|
94
|
+
}): EnhancedTreeItem[] => {
|
|
95
|
+
if (!data.tree) {
|
|
96
|
+
return Object.entries(data.allItems)
|
|
97
|
+
.map((value) => documentPairToNode(value[1]))
|
|
98
|
+
.filter(Boolean) as EnhancedTreeItem[]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return Object.entries(data.allItems)
|
|
102
|
+
.filter(
|
|
103
|
+
([publishedId]) =>
|
|
104
|
+
publishedId &&
|
|
105
|
+
// unadded items shouldn't be in the tree
|
|
106
|
+
!data.tree.some((treeItem) => treeItem?.value?.reference?._ref === publishedId)
|
|
107
|
+
)
|
|
108
|
+
.map(([, documentPair]) => documentPairToNode(documentPair))
|
|
109
|
+
.filter(Boolean) as EnhancedTreeItem[]
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function normalizeNodeForStorage(item: LocalTreeItem): StoredTreeItem {
|
|
113
|
+
return {
|
|
114
|
+
_key: item._key,
|
|
115
|
+
_type: item._type || INTERNAL_NODE_TYPE,
|
|
116
|
+
value: item.value,
|
|
117
|
+
parent: item.parent
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FlatDataItem,
|
|
3
|
+
FullTree,
|
|
4
|
+
getFlatDataFromTree,
|
|
5
|
+
NodeData,
|
|
6
|
+
TreeItem
|
|
7
|
+
} from '@nosferatu500/react-sortable-tree'
|
|
8
|
+
import {randomKey} from '@sanity/util/content'
|
|
9
|
+
import * as Patch from 'sanity'
|
|
10
|
+
import {LocalTreeItem, NodeProps} from '../types'
|
|
11
|
+
import getAdjescentNodes from './getAdjescentNodes'
|
|
12
|
+
import moveItemInArray from './moveItemInArray'
|
|
13
|
+
import {normalizeNodeForStorage} from './treeData'
|
|
14
|
+
|
|
15
|
+
export type HandleMovedNodeData = Omit<
|
|
16
|
+
NodeData & FullTree & any,
|
|
17
|
+
'prevPath' | 'prevTreeIndex' | 'path' | 'treeIndex' | 'node'
|
|
18
|
+
> & {node: LocalTreeItem}
|
|
19
|
+
|
|
20
|
+
export type HandleMovedNode = (moveData: HandleMovedNodeData) => void
|
|
21
|
+
|
|
22
|
+
export function getAddItemPatch(item: LocalTreeItem): unknown[] {
|
|
23
|
+
const normalizedNode = normalizeNodeForStorage(item)
|
|
24
|
+
|
|
25
|
+
return [
|
|
26
|
+
// Add the node to the end of the tree
|
|
27
|
+
Patch.insert([normalizedNode], 'after', [-1])
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getDuplicateItemPatch(nodeProps: NodeProps): unknown[] {
|
|
32
|
+
const newItem = {
|
|
33
|
+
...nodeProps.node,
|
|
34
|
+
_key: randomKey(12)
|
|
35
|
+
}
|
|
36
|
+
const normalizedNode = normalizeNodeForStorage(newItem)
|
|
37
|
+
|
|
38
|
+
return [
|
|
39
|
+
// Add duplicated node before the existing one
|
|
40
|
+
Patch.insert([normalizedNode], 'before', [{_key: nodeProps.node._key}])
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getRemoveItemPatch({node}: Pick<NodeProps, 'node'>): unknown[] {
|
|
45
|
+
const keyPath = {_key: node._key}
|
|
46
|
+
const children = getChildrenPaths(node)
|
|
47
|
+
|
|
48
|
+
return [
|
|
49
|
+
// 1. Unset the removed node
|
|
50
|
+
Patch.unset([keyPath]),
|
|
51
|
+
|
|
52
|
+
// 2. Unset its children
|
|
53
|
+
...children.map((path) => Patch.unset([{_key: path}]))
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getMovedNodePatch(data: HandleMovedNodeData): unknown[] {
|
|
58
|
+
const {nextParentNode} = data
|
|
59
|
+
const keyPath = {_key: data.node._key}
|
|
60
|
+
|
|
61
|
+
// === REMOVING NODE FROM TREE ===
|
|
62
|
+
// `nextPath` will be null if the item is removed from tree
|
|
63
|
+
if (!Array.isArray(data.nextPath)) {
|
|
64
|
+
return getRemoveItemPatch({node: data.node})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const nextFlatTree = getFlatDataFromTree({
|
|
68
|
+
treeData: data.treeData,
|
|
69
|
+
getNodeKey: (t) => t.node._key
|
|
70
|
+
})
|
|
71
|
+
const normalizedNode = normalizeNodeForStorage(data.node)
|
|
72
|
+
|
|
73
|
+
const {leadingNode, followingNode} = getAdjescentNodes({
|
|
74
|
+
flatTree: nextFlatTree,
|
|
75
|
+
node: data.node,
|
|
76
|
+
treeIndex: data.nextTreeIndex
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
// 1. Unset the moved node
|
|
81
|
+
// (will be ignored by Content Lake on new nodes with _key not yet in tree)
|
|
82
|
+
Patch.unset([keyPath]),
|
|
83
|
+
|
|
84
|
+
// 2. SIBLING-BASED PLACEMENT
|
|
85
|
+
// If we were to place solely based on nextTreeIndex, concurrent changes from other editors could put the new node in an unexpected position.
|
|
86
|
+
// Let's instead anchor it to the _key of the sibling coming before or after it.
|
|
87
|
+
leadingNode?.node?._key
|
|
88
|
+
? // After the sibling before it
|
|
89
|
+
Patch.insert([normalizedNode], 'after', [{_key: leadingNode.node._key}])
|
|
90
|
+
: // Or before the sibling right after it, in case there's no leading sibling node
|
|
91
|
+
// prettier-ignore
|
|
92
|
+
Patch.insert([normalizedNode], 'before', [followingNode?.node?._key ? {_key: followingNode.node._key} : data.nextTreeIndex]),
|
|
93
|
+
|
|
94
|
+
// 3. Patch the new node with its new `parent`
|
|
95
|
+
nextParentNode
|
|
96
|
+
? // If it has a parent node, set that parent's _key
|
|
97
|
+
Patch.set(nextParentNode._key, [keyPath, 'parent'])
|
|
98
|
+
: // Else remove the parent key entirely
|
|
99
|
+
Patch.unset([keyPath, 'parent'])
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getChildrenPaths(node: TreeItem): string[] {
|
|
104
|
+
if (!Array.isArray(node.children)) {
|
|
105
|
+
return []
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return node.children
|
|
109
|
+
.reduce(
|
|
110
|
+
(keyPaths, child) => [...keyPaths, child._key, ...getChildrenPaths(child)],
|
|
111
|
+
[] as string[]
|
|
112
|
+
)
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getMoveItemPatch({
|
|
117
|
+
nodeProps: {node, treeIndex},
|
|
118
|
+
localTree,
|
|
119
|
+
direction = 'up'
|
|
120
|
+
}: {
|
|
121
|
+
nodeProps: any
|
|
122
|
+
localTree: TreeItem[]
|
|
123
|
+
direction: 'up' | 'down'
|
|
124
|
+
}): unknown[] {
|
|
125
|
+
const keyPath = {_key: node._key}
|
|
126
|
+
|
|
127
|
+
const nextTreeIndex = treeIndex + (direction === 'up' ? -1 : 1)
|
|
128
|
+
|
|
129
|
+
const flatTree = getFlatDataFromTree({
|
|
130
|
+
treeData: localTree,
|
|
131
|
+
getNodeKey: (t) => t.node._key
|
|
132
|
+
})
|
|
133
|
+
const nextFlatTree = moveItemInArray<FlatDataItem>({
|
|
134
|
+
array: flatTree,
|
|
135
|
+
fromIndex: treeIndex,
|
|
136
|
+
toIndex: nextTreeIndex
|
|
137
|
+
})
|
|
138
|
+
const {leadingNode, followingNode} = getAdjescentNodes({
|
|
139
|
+
flatTree: nextFlatTree,
|
|
140
|
+
node,
|
|
141
|
+
treeIndex: nextTreeIndex
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const normalizedNode = normalizeNodeForStorage(node)
|
|
145
|
+
|
|
146
|
+
// When moving up, look at following node to figure out what is the next parent.
|
|
147
|
+
const nodeToInheritParent = direction === 'up' ? followingNode : leadingNode
|
|
148
|
+
const nextParentNode = nodeToInheritParent?.parentNode
|
|
149
|
+
|
|
150
|
+
return [
|
|
151
|
+
// 1. Unset the moved node
|
|
152
|
+
// (will be ignored by Content Lake on new nodes with _key not yet in tree)
|
|
153
|
+
Patch.unset([keyPath]),
|
|
154
|
+
|
|
155
|
+
// 2. SIBLING-BASED PLACEMENT
|
|
156
|
+
leadingNode?.node?._key
|
|
157
|
+
? // After the sibling before it
|
|
158
|
+
Patch.insert([normalizedNode], 'after', [{_key: leadingNode.node._key}])
|
|
159
|
+
: // Or before the sibling right after it, in case there's no leading sibling node
|
|
160
|
+
Patch.insert([normalizedNode], 'before', [
|
|
161
|
+
followingNode?.node?._key ? {_key: followingNode.node._key} : nextTreeIndex
|
|
162
|
+
]),
|
|
163
|
+
|
|
164
|
+
// 3. Patch the new node with its new `parent`
|
|
165
|
+
nextParentNode
|
|
166
|
+
? // If it has a parent node, set that parent's _key
|
|
167
|
+
Patch.set(nextParentNode._key, [keyPath, 'parent'])
|
|
168
|
+
: // Else remove the parent key entirely
|
|
169
|
+
Patch.unset([keyPath, 'parent'])
|
|
170
|
+
]
|
|
171
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const {showIncompatiblePluginDialog} = require('@sanity/incompatible-plugin')
|
|
2
|
+
const {name, version, sanityExchangeUrl} = require('./package.json')
|
|
3
|
+
|
|
4
|
+
export default showIncompatiblePluginDialog({
|
|
5
|
+
name: name,
|
|
6
|
+
versions: {
|
|
7
|
+
v3: version,
|
|
8
|
+
v2: undefined,
|
|
9
|
+
},
|
|
10
|
+
sanityExchangeUrl,
|
|
11
|
+
})
|
package/.husky/commit-msg
DELETED