@sanity/hierarchical-document-list 1.0.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 +365 -297
- 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 +94 -55
- package/sanity.json +8 -12
- 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/src/components/TreeEditorErrorBoundary.tsx +14 -0
- 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/lib/TreeDeskStructure.d.ts +0 -8
- package/lib/TreeDeskStructure.js +0 -96
- package/lib/TreeInputComponent.d.ts +0 -19
- package/lib/TreeInputComponent.js +0 -52
- 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 -82
- 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/TreeEditor.d.ts +0 -12
- package/lib/components/TreeEditor.js +0 -59
- package/lib/components/TreeEditorErrorBoundary.d.ts +0 -3
- package/lib/components/TreeEditorErrorBoundary.js +0 -59
- 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 -85
- package/lib/createHierarchicalSchemas.d.ts +0 -78
- 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/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 -42
- 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 -58
- 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/screenshot-1.jpg +0 -0
- package/tsconfig.json +0 -20
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import {SanityDocument, useClient} from 'sanity'
|
|
3
|
+
import {AllItems, TreeInputOptions} from '../types'
|
|
4
|
+
import {isDraft, unprefixId} from '../utils/idUtils'
|
|
5
|
+
|
|
6
|
+
function getDeskFilter({referenceTo, referenceOptions}: TreeInputOptions): {
|
|
7
|
+
filter: string
|
|
8
|
+
params: Record<string, unknown>
|
|
9
|
+
} {
|
|
10
|
+
const filterParts: string[] = ['_type in $docTypes']
|
|
11
|
+
|
|
12
|
+
if (referenceOptions?.filter) {
|
|
13
|
+
filterParts.push(referenceOptions.filter)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
filter: filterParts.join(' && '),
|
|
18
|
+
params: {
|
|
19
|
+
...(referenceOptions?.filterParams || {}),
|
|
20
|
+
docTypes: referenceTo.map((schemaType) => schemaType)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type Status = 'loading' | 'success' | 'error'
|
|
26
|
+
|
|
27
|
+
type ACTIONTYPE =
|
|
28
|
+
| {type: 'addOrEditItem'; item: SanityDocument}
|
|
29
|
+
| {type: 'removeItem'; itemId: string}
|
|
30
|
+
| {type: 'setInitialData'; items: SanityDocument[]}
|
|
31
|
+
|
|
32
|
+
function updateItemInState(state: AllItems, item: SanityDocument): AllItems {
|
|
33
|
+
const newState = {...state}
|
|
34
|
+
const publishedId = unprefixId(item._id)
|
|
35
|
+
newState[publishedId] = {
|
|
36
|
+
...(newState[publishedId] || {}),
|
|
37
|
+
[isDraft(item._id) ? 'draft' : 'published']: item
|
|
38
|
+
}
|
|
39
|
+
return newState
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function allItemsReducer(state: AllItems, action: ACTIONTYPE): AllItems {
|
|
43
|
+
if (action.type === 'addOrEditItem' && action.item?._id) {
|
|
44
|
+
return updateItemInState(state, action.item)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (action.type === 'removeItem') {
|
|
48
|
+
const publishedId = unprefixId(action.itemId)
|
|
49
|
+
return {
|
|
50
|
+
...state,
|
|
51
|
+
[publishedId]: isDraft(action.itemId)
|
|
52
|
+
? // If a draft, keep only published
|
|
53
|
+
{
|
|
54
|
+
published: state[publishedId]?.published
|
|
55
|
+
}
|
|
56
|
+
: {
|
|
57
|
+
draft: state[publishedId]?.draft
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (action.type === 'setInitialData') {
|
|
63
|
+
return action.items.reduce(updateItemInState, {} as AllItems)
|
|
64
|
+
}
|
|
65
|
+
return state
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default function useAllItems(options: TreeInputOptions): {
|
|
69
|
+
status: Status
|
|
70
|
+
allItems: AllItems
|
|
71
|
+
} {
|
|
72
|
+
const client = useClient({
|
|
73
|
+
apiVersion: '2021-09-01'
|
|
74
|
+
})
|
|
75
|
+
const [status, setStatus] = React.useState<Status>('loading')
|
|
76
|
+
const [allItems, dispatch] = React.useReducer(allItemsReducer, {})
|
|
77
|
+
|
|
78
|
+
function handleListener(event: any) {
|
|
79
|
+
if (event.type !== 'mutation') {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (event.result) {
|
|
84
|
+
dispatch({type: 'addOrEditItem', item: event.result})
|
|
85
|
+
} else {
|
|
86
|
+
dispatch({type: 'removeItem', itemId: event.documentId})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleFirstLoad(items: SanityDocument[]) {
|
|
91
|
+
dispatch({type: 'setInitialData', items})
|
|
92
|
+
setStatus('success')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
React.useEffect(() => {
|
|
96
|
+
const {filter, params} = getDeskFilter(options)
|
|
97
|
+
const query = `*[${filter}] {
|
|
98
|
+
_id,
|
|
99
|
+
_type,
|
|
100
|
+
_updatedAt,
|
|
101
|
+
}`
|
|
102
|
+
client
|
|
103
|
+
.fetch<SanityDocument[]>(query, params)
|
|
104
|
+
.then(handleFirstLoad)
|
|
105
|
+
.catch(() => {
|
|
106
|
+
setStatus('error')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const listener = client.listen(query, params).subscribe(handleListener)
|
|
110
|
+
return () => {
|
|
111
|
+
listener.unsubscribe()
|
|
112
|
+
}
|
|
113
|
+
}, [])
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
status,
|
|
117
|
+
allItems
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import {AllItems, LocalTreeItem, StoredTreeItem, VisibilityMap} from '../types'
|
|
3
|
+
import {dataToEditorTree} from '../utils/treeData'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Enhances tree data with information on:
|
|
7
|
+
* - `expanded` - native property of react-sortable-tree to determine collapsing & expanding of a node's children
|
|
8
|
+
* - `draftId` & `publishedId` - refer to LocalTreeItem's type annotations
|
|
9
|
+
*
|
|
10
|
+
* Doesn't modify the main tree or has side-effects on data.
|
|
11
|
+
* Has the added benefit of being local to the user, so external changes won't affect local visibility.
|
|
12
|
+
*/
|
|
13
|
+
export default function useLocalTree({
|
|
14
|
+
tree,
|
|
15
|
+
allItems,
|
|
16
|
+
}: {
|
|
17
|
+
tree: StoredTreeItem[]
|
|
18
|
+
allItems: AllItems
|
|
19
|
+
}): {
|
|
20
|
+
handleVisibilityToggle: (data: any) => void
|
|
21
|
+
localTree: LocalTreeItem[]
|
|
22
|
+
} {
|
|
23
|
+
const [visibilityMap, setVisibilityMap] = React.useState<VisibilityMap>({})
|
|
24
|
+
|
|
25
|
+
function handleVisibilityToggle(data: any) {
|
|
26
|
+
setVisibilityMap({
|
|
27
|
+
...visibilityMap,
|
|
28
|
+
[data.node._key]: data.expanded,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
localTree: dataToEditorTree({
|
|
34
|
+
tree,
|
|
35
|
+
allItems,
|
|
36
|
+
visibilityMap,
|
|
37
|
+
}),
|
|
38
|
+
handleVisibilityToggle,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import useAllItems from './useAllItems'
|
|
3
|
+
import useTreeOperationsProvider from './useTreeOperationsProvider'
|
|
4
|
+
|
|
5
|
+
type ContextValue = ReturnType<typeof useTreeOperationsProvider> & {
|
|
6
|
+
allItemsStatus: ReturnType<typeof useAllItems>['status']
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function placeholder() {
|
|
10
|
+
// no-op
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const TreeOperationsContext = React.createContext<ContextValue>({
|
|
14
|
+
addItem: placeholder,
|
|
15
|
+
duplicateItem: placeholder,
|
|
16
|
+
removeItem: placeholder,
|
|
17
|
+
handleMovedNode: placeholder,
|
|
18
|
+
moveItemDown: placeholder,
|
|
19
|
+
moveItemUp: placeholder,
|
|
20
|
+
allItemsStatus: 'loading',
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
export default function useTreeOperations(): ContextValue {
|
|
24
|
+
return React.useContext(TreeOperationsContext)
|
|
25
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {PatchEvent, PathSegment, prefixPath, setIfMissing} from 'sanity'
|
|
2
|
+
import {LocalTreeItem, NodeProps} from '../types'
|
|
3
|
+
import {
|
|
4
|
+
HandleMovedNode,
|
|
5
|
+
HandleMovedNodeData,
|
|
6
|
+
getAddItemPatch,
|
|
7
|
+
getDuplicateItemPatch,
|
|
8
|
+
getMoveItemPatch,
|
|
9
|
+
getMovedNodePatch,
|
|
10
|
+
getRemoveItemPatch
|
|
11
|
+
} from '../utils/treePatches'
|
|
12
|
+
|
|
13
|
+
export default function useTreeOperationsProvider(props: {
|
|
14
|
+
patchPrefix?: PathSegment
|
|
15
|
+
onChange: (patch: PatchEvent) => void
|
|
16
|
+
localTree: LocalTreeItem[]
|
|
17
|
+
}): {
|
|
18
|
+
handleMovedNode: HandleMovedNode
|
|
19
|
+
addItem: (item: LocalTreeItem) => void
|
|
20
|
+
duplicateItem: (nodeProps: NodeProps) => void
|
|
21
|
+
removeItem: (nodeProps: NodeProps) => void
|
|
22
|
+
moveItemUp: (nodeProps: NodeProps) => void
|
|
23
|
+
moveItemDown: (nodeProps: NodeProps) => void
|
|
24
|
+
} {
|
|
25
|
+
const {localTree} = props
|
|
26
|
+
|
|
27
|
+
function runPatches(patches: any) {
|
|
28
|
+
const finalPatches = [
|
|
29
|
+
// Ensure tree array exists before any operation
|
|
30
|
+
setIfMissing([]),
|
|
31
|
+
...(patches || [])
|
|
32
|
+
]
|
|
33
|
+
let patchEvent = PatchEvent.from(finalPatches)
|
|
34
|
+
if (props.patchPrefix) {
|
|
35
|
+
patchEvent = PatchEvent.from(
|
|
36
|
+
finalPatches.map((patch) => prefixPath(patch, props.patchPrefix as PathSegment))
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
props.onChange(patchEvent)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function handleMovedNode(data: HandleMovedNodeData & {node: LocalTreeItem}) {
|
|
43
|
+
runPatches(getMovedNodePatch(data))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function addItem(item: LocalTreeItem) {
|
|
47
|
+
runPatches(getAddItemPatch(item))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function duplicateItem(nodeProps: NodeProps & {node: LocalTreeItem}) {
|
|
51
|
+
runPatches(getDuplicateItemPatch(nodeProps))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function removeItem(nodeProps: NodeProps) {
|
|
55
|
+
runPatches(getRemoveItemPatch(nodeProps))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function moveItemUp(nodeProps: NodeProps) {
|
|
59
|
+
runPatches(
|
|
60
|
+
getMoveItemPatch({
|
|
61
|
+
nodeProps,
|
|
62
|
+
localTree,
|
|
63
|
+
direction: 'up'
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function moveItemDown(nodeProps: NodeProps) {
|
|
69
|
+
runPatches(
|
|
70
|
+
getMoveItemPatch({
|
|
71
|
+
nodeProps,
|
|
72
|
+
localTree,
|
|
73
|
+
direction: 'down'
|
|
74
|
+
})
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
handleMovedNode,
|
|
80
|
+
addItem,
|
|
81
|
+
removeItem,
|
|
82
|
+
moveItemUp,
|
|
83
|
+
moveItemDown,
|
|
84
|
+
duplicateItem
|
|
85
|
+
}
|
|
86
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {definePlugin} from 'sanity'
|
|
2
|
+
import {default as createDeskHierarchy, type TreeProps} from './createDeskHierarchy'
|
|
3
|
+
import {default as createHierarchicalSchemas} from './createHierarchicalSchemas'
|
|
4
|
+
import {default as hierarchyTree} from './schemas/hierarchy.tree'
|
|
5
|
+
import {default as flatDataToTree} from './utils/flatDataToTree'
|
|
6
|
+
|
|
7
|
+
export {createDeskHierarchy, createHierarchicalSchemas, flatDataToTree, hierarchyTree, TreeProps}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Usage in `sanity.config.ts` (or .js)
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* import {defineConfig} from 'sanity'
|
|
14
|
+
* import {myPlugin} from 'sanity-plugin-hdl-sanity'
|
|
15
|
+
*
|
|
16
|
+
* export default defineConfig({
|
|
17
|
+
* // ...
|
|
18
|
+
* plugins: [myPlugin()],
|
|
19
|
+
* })
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const hierarchicalDocumentList = definePlugin({
|
|
24
|
+
name: 'sanity-plugin-hierarchical-document-list'
|
|
25
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {defineType} from 'sanity'
|
|
2
|
+
|
|
3
|
+
export default defineType({
|
|
4
|
+
name: 'hierarchy.tree',
|
|
5
|
+
title: 'Hierarchical tree',
|
|
6
|
+
type: 'document',
|
|
7
|
+
// The plugin needs to define a `schemaType` with liveEdit enabled so that
|
|
8
|
+
// `useDocumentOperation` in TreeDeskStructure.tsx doesn't create drafts at every patch.
|
|
9
|
+
liveEdit: true,
|
|
10
|
+
// Let's avoid defining the actual hierarchical field, else GraphQL users won't be able to deploy schemas
|
|
11
|
+
fields: [
|
|
12
|
+
{
|
|
13
|
+
name: 'unusedField',
|
|
14
|
+
title: 'Unused field',
|
|
15
|
+
type: 'string',
|
|
16
|
+
hidden: true,
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
})
|