@sanity/orderable-document-list 0.0.10 → 1.0.0-v3-studio.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 +136 -54
- package/lib/index.d.ts +38 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +849 -30
- package/lib/index.js.map +1 -1
- package/lib/index.modern.js +840 -0
- package/lib/index.modern.js.map +1 -0
- package/package.json +56 -22
- package/src/{Document.js → Document.tsx} +30 -27
- package/src/{DocumentListQuery.js → DocumentListQuery.tsx} +27 -23
- package/src/{DocumentListWrapper.js → DocumentListWrapper.tsx} +24 -29
- package/src/DraggableList.tsx +304 -0
- package/src/{Feedback.js → Feedback.tsx} +2 -7
- package/src/OrderableContext.ts +7 -0
- package/src/{OrderableDocumentList.js → OrderableDocumentList.tsx} +23 -12
- package/src/desk-structure/globalClientWorkaround.ts +33 -0
- package/src/desk-structure/{orderableDocumentListDeskItem.js → orderableDocumentListDeskItem.ts} +26 -10
- package/src/fields/orderRankField.ts +45 -0
- package/src/fields/{orderRankOrdering.js → orderRankOrdering.ts} +0 -0
- package/src/helpers/client.ts +13 -0
- package/src/helpers/constants.ts +1 -0
- package/src/helpers/{initialRank.js → initialRank.ts} +2 -2
- package/src/helpers/{reorderDocuments.js → reorderDocuments.ts} +33 -44
- package/src/helpers/{resetOrder.js → resetOrder.ts} +3 -8
- package/src/index.ts +9 -0
- package/.babelrc +0 -3
- package/.eslintignore +0 -1
- package/.eslintrc.js +0 -50
- package/lib/Document.js +0 -101
- package/lib/Document.js.map +0 -1
- package/lib/DocumentListQuery.js +0 -163
- package/lib/DocumentListQuery.js.map +0 -1
- package/lib/DocumentListWrapper.js +0 -107
- package/lib/DocumentListWrapper.js.map +0 -1
- package/lib/DraggableList.js +0 -314
- package/lib/DraggableList.js.map +0 -1
- package/lib/Feedback.js +0 -31
- package/lib/Feedback.js.map +0 -1
- package/lib/OrderableContext.js +0 -15
- package/lib/OrderableContext.js.map +0 -1
- package/lib/OrderableDocumentList.js +0 -103
- package/lib/OrderableDocumentList.js.map +0 -1
- package/lib/desk-structure/orderableDocumentListDeskItem.js +0 -57
- package/lib/desk-structure/orderableDocumentListDeskItem.js.map +0 -1
- package/lib/fields/orderRankField.js +0 -64
- package/lib/fields/orderRankField.js.map +0 -1
- package/lib/fields/orderRankOrdering.js +0 -19
- package/lib/fields/orderRankOrdering.js.map +0 -1
- package/lib/helpers/constants.js +0 -9
- package/lib/helpers/constants.js.map +0 -1
- package/lib/helpers/initialRank.js +0 -18
- package/lib/helpers/initialRank.js.map +0 -1
- package/lib/helpers/reorderDocuments.js +0 -131
- package/lib/helpers/reorderDocuments.js.map +0 -1
- package/lib/helpers/resetOrder.js +0 -62
- package/lib/helpers/resetOrder.js.map +0 -1
- package/sanity.json +0 -7
- package/src/DraggableList.js +0 -276
- package/src/OrderableContext.js +0 -3
- package/src/fields/orderRankField.js +0 -35
- package/src/helpers/constants.js +0 -1
- package/src/index.js +0 -5
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import React, {useEffect, useState, useMemo, useCallback, CSSProperties} from 'react'
|
|
2
|
+
import {DragDropContext, Draggable, Droppable, type DropResult} from 'react-beautiful-dnd'
|
|
3
|
+
import {Box, Card, useToast} from '@sanity/ui'
|
|
4
|
+
import {usePaneRouter} from 'sanity/desk'
|
|
5
|
+
import type {SanityDocument, PatchOperations} from 'sanity'
|
|
6
|
+
import Document from './Document'
|
|
7
|
+
import {reorderDocuments} from './helpers/reorderDocuments'
|
|
8
|
+
import {ORDER_FIELD_NAME} from './helpers/constants'
|
|
9
|
+
import {useSanityClient} from './helpers/client'
|
|
10
|
+
|
|
11
|
+
const getItemStyle = (
|
|
12
|
+
draggableStyle: CSSProperties | undefined,
|
|
13
|
+
itemIsUpdating: boolean
|
|
14
|
+
): CSSProperties => ({
|
|
15
|
+
userSelect: 'none',
|
|
16
|
+
transition: `opacity 500ms ease-in-out`,
|
|
17
|
+
opacity: itemIsUpdating ? 0.2 : 1,
|
|
18
|
+
pointerEvents: itemIsUpdating ? `none` : undefined,
|
|
19
|
+
...draggableStyle,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const cardTone = (settings: ListSetting) => {
|
|
23
|
+
const {isDuplicate, isGhosting, isDragging, isSelected} = settings
|
|
24
|
+
|
|
25
|
+
if (isGhosting) return `transparent`
|
|
26
|
+
if (isDragging || isSelected) return `primary`
|
|
27
|
+
if (isDuplicate) return `caution`
|
|
28
|
+
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ListSetting {
|
|
33
|
+
isDuplicate: boolean
|
|
34
|
+
isGhosting: boolean
|
|
35
|
+
isDragging: boolean
|
|
36
|
+
isSelected: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DraggableListProps {
|
|
40
|
+
data: SanityDocument[]
|
|
41
|
+
type: string
|
|
42
|
+
listIsUpdating: boolean
|
|
43
|
+
setListIsUpdating: (val: boolean) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default function DraggableList({
|
|
47
|
+
data,
|
|
48
|
+
type,
|
|
49
|
+
listIsUpdating,
|
|
50
|
+
setListIsUpdating,
|
|
51
|
+
}: DraggableListProps) {
|
|
52
|
+
const toast = useToast()
|
|
53
|
+
const router = usePaneRouter()
|
|
54
|
+
const {navigateIntent} = router
|
|
55
|
+
|
|
56
|
+
// Maintains local state order before transaction completes
|
|
57
|
+
const [orderedData, setOrderedData] = useState<SanityDocument[]>(data)
|
|
58
|
+
|
|
59
|
+
// Update local state when documents change from an outside source
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!listIsUpdating) setOrderedData(data)
|
|
62
|
+
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
|
63
|
+
}, [data])
|
|
64
|
+
|
|
65
|
+
const [draggingId, setDraggingId] = useState(``)
|
|
66
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
|
67
|
+
|
|
68
|
+
const clearSelected = useCallback(() => setSelectedIds([]), [setSelectedIds])
|
|
69
|
+
|
|
70
|
+
const handleSelect = useCallback(
|
|
71
|
+
(clickedId: string, index: number, nativeEvent: MouseEvent) => {
|
|
72
|
+
const isSelected = selectedIds.includes(clickedId)
|
|
73
|
+
const selectMultiple = nativeEvent.shiftKey
|
|
74
|
+
const isUsingWindows = navigator.appVersion.indexOf('Win') !== -1
|
|
75
|
+
const selectAdditional = isUsingWindows ? nativeEvent.ctrlKey : nativeEvent.metaKey
|
|
76
|
+
|
|
77
|
+
let updatedIds = []
|
|
78
|
+
|
|
79
|
+
// No modifier keys pressed during click:
|
|
80
|
+
// - update selected to just this one
|
|
81
|
+
// - open document
|
|
82
|
+
if (!selectMultiple && !selectAdditional) {
|
|
83
|
+
navigateIntent('edit', {id: clickedId, type})
|
|
84
|
+
return setSelectedIds([clickedId])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Shift key was held, add id's between last selected and this one
|
|
88
|
+
// ...before adding this one
|
|
89
|
+
if (selectMultiple && !isSelected) {
|
|
90
|
+
const lastSelectedId = selectedIds[selectedIds.length - 1]
|
|
91
|
+
const lastSelectedIndex = orderedData.findIndex((item) => item._id === lastSelectedId)
|
|
92
|
+
|
|
93
|
+
const firstSelected = index < lastSelectedIndex ? index : lastSelectedIndex
|
|
94
|
+
const lastSelected = index > lastSelectedIndex ? index : lastSelectedIndex
|
|
95
|
+
|
|
96
|
+
const betweenIds = orderedData
|
|
97
|
+
.filter((item, itemIndex) => itemIndex > firstSelected && itemIndex < lastSelected)
|
|
98
|
+
.map((item) => item._id)
|
|
99
|
+
|
|
100
|
+
updatedIds = [...selectedIds, ...betweenIds, clickedId]
|
|
101
|
+
} else if (isSelected) {
|
|
102
|
+
// Toggle off a single id
|
|
103
|
+
updatedIds = selectedIds.filter((id) => id !== clickedId)
|
|
104
|
+
} else {
|
|
105
|
+
// Toggle on a single id
|
|
106
|
+
updatedIds = [...selectedIds, clickedId]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return setSelectedIds(updatedIds)
|
|
110
|
+
},
|
|
111
|
+
[setSelectedIds, navigateIntent, orderedData, selectedIds, type]
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const client = useSanityClient()
|
|
115
|
+
|
|
116
|
+
const transactPatches = useCallback(
|
|
117
|
+
async (patches: [string, PatchOperations][], message: string) => {
|
|
118
|
+
const transaction = client.transaction()
|
|
119
|
+
|
|
120
|
+
patches.forEach(([docId, ops]) => transaction.patch(docId, ops))
|
|
121
|
+
|
|
122
|
+
await transaction
|
|
123
|
+
.commit()
|
|
124
|
+
.then((updated) => {
|
|
125
|
+
clearSelected()
|
|
126
|
+
setDraggingId(``)
|
|
127
|
+
setListIsUpdating(false)
|
|
128
|
+
toast.push({
|
|
129
|
+
title: `${
|
|
130
|
+
updated.results.length === 1 ? `1 Document` : `${updated.results.length} Documents`
|
|
131
|
+
} Reordered`,
|
|
132
|
+
status: `success`,
|
|
133
|
+
description: message,
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
.catch(() => {
|
|
137
|
+
setDraggingId(``)
|
|
138
|
+
setListIsUpdating(false)
|
|
139
|
+
toast.push({
|
|
140
|
+
title: `Reordering failed`,
|
|
141
|
+
status: `error`,
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
},
|
|
145
|
+
[client, setDraggingId, clearSelected, setListIsUpdating, toast]
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const handleDragEnd = useCallback(
|
|
149
|
+
(result: DropResult | undefined, entities: SanityDocument[]) => {
|
|
150
|
+
setDraggingId(``)
|
|
151
|
+
|
|
152
|
+
const {source, destination, draggableId} = result ?? {}
|
|
153
|
+
|
|
154
|
+
// Don't do anything if nothing changed
|
|
155
|
+
if (source?.index === destination?.index) return
|
|
156
|
+
|
|
157
|
+
// Don't do anything if we don't have the entitites
|
|
158
|
+
if (!entities?.length || !draggableId) return
|
|
159
|
+
|
|
160
|
+
// A document can be dragged without being one-of-many-selected
|
|
161
|
+
const effectedIds = selectedIds?.length ? selectedIds : [draggableId]
|
|
162
|
+
|
|
163
|
+
// Don't do anything if we don't have ids to effect
|
|
164
|
+
if (!effectedIds?.length) return
|
|
165
|
+
|
|
166
|
+
// Update state to update styles + prevent data refetching
|
|
167
|
+
setListIsUpdating(true)
|
|
168
|
+
setSelectedIds(effectedIds)
|
|
169
|
+
|
|
170
|
+
const {newOrder, patches, message} = reorderDocuments({
|
|
171
|
+
entities,
|
|
172
|
+
selectedIds: effectedIds,
|
|
173
|
+
source,
|
|
174
|
+
destination,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
// Update local state
|
|
178
|
+
if (newOrder?.length) {
|
|
179
|
+
setOrderedData(newOrder as any)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Transact new order patches
|
|
183
|
+
if (patches?.length) {
|
|
184
|
+
transactPatches(patches, message)
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
[selectedIds, setDraggingId, setSelectedIds, transactPatches, setListIsUpdating]
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
const handleDragStart = useCallback(
|
|
191
|
+
(start: {draggableId: string}) => {
|
|
192
|
+
const id = start.draggableId
|
|
193
|
+
const selected = selectedIds.includes(id)
|
|
194
|
+
|
|
195
|
+
// if dragging an item that is not selected - unselect all items
|
|
196
|
+
if (!selected) clearSelected()
|
|
197
|
+
|
|
198
|
+
setDraggingId(id)
|
|
199
|
+
},
|
|
200
|
+
[selectedIds, clearSelected, setDraggingId]
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
// Move one document up or down one place, by fake invoking the drag function
|
|
204
|
+
const incrementIndex = useCallback(
|
|
205
|
+
(shiftFrom: number, shiftTo: number, id: string, entities: SanityDocument[]) => {
|
|
206
|
+
const result = {
|
|
207
|
+
draggableId: id,
|
|
208
|
+
source: {index: shiftFrom},
|
|
209
|
+
destination: {index: shiftTo},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return handleDragEnd(result as DropResult, entities)
|
|
213
|
+
},
|
|
214
|
+
[handleDragEnd]
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
const onWindowKeyDown = useCallback(
|
|
218
|
+
(event: KeyboardEvent) => {
|
|
219
|
+
if (event.key === 'Escape') {
|
|
220
|
+
clearSelected()
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
[clearSelected]
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
window.addEventListener('keydown', onWindowKeyDown)
|
|
228
|
+
|
|
229
|
+
return () => {
|
|
230
|
+
window.removeEventListener('keydown', onWindowKeyDown)
|
|
231
|
+
}
|
|
232
|
+
}, [onWindowKeyDown])
|
|
233
|
+
|
|
234
|
+
// Find all items with duplicate order field
|
|
235
|
+
const duplicateOrders = useMemo(() => {
|
|
236
|
+
if (!orderedData.length) return []
|
|
237
|
+
|
|
238
|
+
const orderField = orderedData.map((item) => item[ORDER_FIELD_NAME])
|
|
239
|
+
|
|
240
|
+
return orderField.filter((item, index) => orderField.indexOf(item) !== index)
|
|
241
|
+
}, [orderedData])
|
|
242
|
+
|
|
243
|
+
const onDragEnd = useCallback(
|
|
244
|
+
(result: DropResult) => handleDragEnd(result, orderedData),
|
|
245
|
+
[orderedData, handleDragEnd]
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
<DragDropContext onDragStart={handleDragStart} onDragEnd={onDragEnd}>
|
|
250
|
+
<Droppable droppableId="documentSortZone">
|
|
251
|
+
{(provided) => (
|
|
252
|
+
<div {...provided.droppableProps} ref={provided.innerRef}>
|
|
253
|
+
{orderedData.map((item, index) => (
|
|
254
|
+
<Draggable
|
|
255
|
+
key={`${item._id}-${item[ORDER_FIELD_NAME]}`}
|
|
256
|
+
draggableId={item._id}
|
|
257
|
+
index={index}
|
|
258
|
+
// onClick={(event) => handleDraggableClick(event, provided, snapshot)}
|
|
259
|
+
>
|
|
260
|
+
{(innerProvided, innerSnapshot) => {
|
|
261
|
+
const isSelected = selectedIds.includes(item._id)
|
|
262
|
+
const isDragging = innerSnapshot.isDragging
|
|
263
|
+
const isGhosting = Boolean(!isDragging && draggingId && isSelected)
|
|
264
|
+
const isUpdating = listIsUpdating && isSelected
|
|
265
|
+
const isDisabled = Boolean(!item[ORDER_FIELD_NAME])
|
|
266
|
+
const isDuplicate = duplicateOrders.includes(item[ORDER_FIELD_NAME])
|
|
267
|
+
const tone = cardTone({isDuplicate, isGhosting, isDragging, isSelected})
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div
|
|
271
|
+
ref={innerProvided.innerRef}
|
|
272
|
+
{...innerProvided.draggableProps}
|
|
273
|
+
{...innerProvided.dragHandleProps}
|
|
274
|
+
style={
|
|
275
|
+
isDisabled
|
|
276
|
+
? {opacity: 0.2, pointerEvents: `none`}
|
|
277
|
+
: getItemStyle(innerProvided.draggableProps.style, isUpdating)
|
|
278
|
+
}
|
|
279
|
+
>
|
|
280
|
+
<Box paddingBottom={1}>
|
|
281
|
+
<Card tone={tone} shadow={isDragging ? 2 : undefined} radius={2}>
|
|
282
|
+
<Document
|
|
283
|
+
doc={item}
|
|
284
|
+
entities={orderedData}
|
|
285
|
+
handleSelect={handleSelect}
|
|
286
|
+
increment={incrementIndex}
|
|
287
|
+
index={index}
|
|
288
|
+
isFirst={index === 0}
|
|
289
|
+
isLast={index === orderedData.length - 1}
|
|
290
|
+
/>
|
|
291
|
+
</Card>
|
|
292
|
+
</Box>
|
|
293
|
+
</div>
|
|
294
|
+
)
|
|
295
|
+
}}
|
|
296
|
+
</Draggable>
|
|
297
|
+
))}
|
|
298
|
+
{provided.placeholder}
|
|
299
|
+
</div>
|
|
300
|
+
)}
|
|
301
|
+
</Droppable>
|
|
302
|
+
</DragDropContext>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import React from 'react'
|
|
1
|
+
import React, {PropsWithChildren} from 'react'
|
|
3
2
|
import {Box, Card, Text} from '@sanity/ui'
|
|
4
3
|
|
|
5
|
-
export default function Feedback({children}) {
|
|
4
|
+
export default function Feedback({children}: PropsWithChildren<{}>) {
|
|
6
5
|
return (
|
|
7
6
|
<Box padding={3}>
|
|
8
7
|
<Card padding={4} radius={2} shadow={1} tone="caution">
|
|
@@ -11,7 +10,3 @@ export default function Feedback({children}) {
|
|
|
11
10
|
</Box>
|
|
12
11
|
)
|
|
13
12
|
}
|
|
14
|
-
|
|
15
|
-
Feedback.propTypes = {
|
|
16
|
-
children: PropTypes.node.isRequired,
|
|
17
|
-
}
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import PropTypes from 'prop-types'
|
|
2
1
|
import React, {Component} from 'react'
|
|
3
2
|
|
|
3
|
+
import {SanityClient} from '@sanity/client'
|
|
4
|
+
import type {ToastParams} from '@sanity/ui'
|
|
4
5
|
import DocumentListWrapper from './DocumentListWrapper'
|
|
5
6
|
import {resetOrder} from './helpers/resetOrder'
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
params: PropTypes.object,
|
|
14
|
-
}).isRequired,
|
|
8
|
+
export interface OrderableDocumentListProps {
|
|
9
|
+
options: {
|
|
10
|
+
type: string
|
|
11
|
+
client: SanityClient,
|
|
12
|
+
filter?: string,
|
|
13
|
+
params?: Record<string, unknown>
|
|
15
14
|
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface State {
|
|
18
|
+
showIncrements: boolean
|
|
19
|
+
resetOrderTransaction: ToastParams
|
|
20
|
+
}
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
// Must use a Class Component here so the actionHandlers can be called
|
|
23
|
+
export default class OrderableDocumentList extends Component<OrderableDocumentListProps, State> {
|
|
24
|
+
constructor(props: OrderableDocumentListProps) {
|
|
18
25
|
super(props)
|
|
19
26
|
this.state = {
|
|
20
27
|
showIncrements: false,
|
|
@@ -38,7 +45,7 @@ export default class OrderableDocumentList extends Component {
|
|
|
38
45
|
},
|
|
39
46
|
}))
|
|
40
47
|
|
|
41
|
-
const update = await resetOrder(this.props.options.type)
|
|
48
|
+
const update = await resetOrder(this.props.options.type, this.props.options.client)
|
|
42
49
|
|
|
43
50
|
const reorderWasSuccessful = update?.results?.length
|
|
44
51
|
|
|
@@ -55,11 +62,15 @@ export default class OrderableDocumentList extends Component {
|
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
render() {
|
|
65
|
+
const type = this?.props?.options?.type
|
|
66
|
+
if (!type) {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
58
69
|
return (
|
|
59
70
|
<DocumentListWrapper
|
|
60
|
-
type={this?.props?.options?.type}
|
|
61
71
|
filter={this?.props?.options?.filter}
|
|
62
72
|
params={this?.props?.options?.params}
|
|
73
|
+
type={type}
|
|
63
74
|
showIncrements={this.state.showIncrements}
|
|
64
75
|
resetOrderTransaction={this.state.resetOrderTransaction}
|
|
65
76
|
/>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {SanityClient} from '@sanity/client'
|
|
2
|
+
import type {ConfigContext} from 'sanity'
|
|
3
|
+
import {SchemaContext} from '../fields/orderRankField'
|
|
4
|
+
|
|
5
|
+
type ClientSubscription = (client: SanityClient) => void
|
|
6
|
+
type ClientKey = `${string}:${string}`
|
|
7
|
+
const subscriptions: Record<ClientKey, ClientSubscription[]> = {}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @internal
|
|
11
|
+
*
|
|
12
|
+
* v3 does not expose client in the schema API, which means no client for the initial value code.
|
|
13
|
+
* This is a workaround for that; we update the global client from a place we _do_ have access to the client,
|
|
14
|
+
* the use the global in schema-callbacks.
|
|
15
|
+
*
|
|
16
|
+
* Note: The code assumes sanityClientChanged is called AFTER all subscribeSanityClient calls are done.
|
|
17
|
+
*/
|
|
18
|
+
export function sanityClientChanged(context: ConfigContext) {
|
|
19
|
+
;(subscriptions[clientKey(context)] ?? []).forEach((subscriber) => subscriber(context.client))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export function subscribeSanityClient(context: SchemaContext, callback: ClientSubscription) {
|
|
26
|
+
const key = clientKey(context)
|
|
27
|
+
const existing = subscriptions[key] ?? []
|
|
28
|
+
subscriptions[key] = [...existing, callback]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clientKey(context: SchemaContext): ClientKey {
|
|
32
|
+
return `${context.projectId}:${context.dataset}`
|
|
33
|
+
}
|
package/src/desk-structure/{orderableDocumentListDeskItem.js → orderableDocumentListDeskItem.ts}
RENAMED
|
@@ -1,26 +1,42 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import schema from 'part:@sanity/base/schema'
|
|
1
|
+
import {GenerateIcon, SortIcon} from '@sanity/icons'
|
|
2
|
+
import type {ConfigContext} from 'sanity'
|
|
4
3
|
|
|
4
|
+
import {ComponentType} from 'react'
|
|
5
|
+
import {StructureBuilder} from 'sanity/desk'
|
|
5
6
|
import OrderableDocumentList from '../OrderableDocumentList'
|
|
7
|
+
import {sanityClientChanged} from './globalClientWorkaround'
|
|
6
8
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
+
export interface OrderableListConfig {
|
|
10
|
+
type: string
|
|
11
|
+
id?: string
|
|
12
|
+
title?: string
|
|
13
|
+
icon?: ComponentType
|
|
14
|
+
params?: Record<string, unknown>
|
|
15
|
+
filter?: string
|
|
16
|
+
context: ConfigContext
|
|
17
|
+
S: StructureBuilder
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function orderableDocumentListDeskItem(config: OrderableListConfig) {
|
|
21
|
+
if (!config?.type || !config.context || !config.S) {
|
|
9
22
|
throw new Error(`
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
type, context and S (StructureBuilder) must be provided.
|
|
24
|
+
context and S are available when configuring structure.
|
|
12
25
|
Example: orderableDocumentListDeskItem({type: 'category'})
|
|
13
26
|
`)
|
|
14
27
|
}
|
|
15
28
|
|
|
16
|
-
const {type, filter, params, title, icon, id} = config
|
|
29
|
+
const {type, filter, params, title, icon, id, context, S} = config
|
|
30
|
+
const {schema, client} = context
|
|
31
|
+
// workaround so schemas can get access to client in callbacks
|
|
32
|
+
sanityClientChanged(context)
|
|
17
33
|
|
|
18
34
|
const listTitle = title ?? `Orderable ${type}`
|
|
19
35
|
const listId = id ?? `orderable-${type}`
|
|
20
36
|
const listIcon = icon ?? SortIcon
|
|
21
37
|
const typeTitle = schema.get(type)?.title ?? type
|
|
22
38
|
|
|
23
|
-
return S.listItem(
|
|
39
|
+
return S.listItem()
|
|
24
40
|
.title(listTitle)
|
|
25
41
|
.id(listId)
|
|
26
42
|
.icon(listIcon)
|
|
@@ -33,7 +49,7 @@ export function orderableDocumentListDeskItem(config = {}) {
|
|
|
33
49
|
|
|
34
50
|
type: 'component',
|
|
35
51
|
component: OrderableDocumentList,
|
|
36
|
-
options: {type, filter, params},
|
|
52
|
+
options: {type, filter, params, client},
|
|
37
53
|
menuItems: [
|
|
38
54
|
S.menuItem()
|
|
39
55
|
.title(`Create new ${typeTitle}`)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {ConfigContext} from 'sanity'
|
|
2
|
+
import type {SanityClient} from '@sanity/client'
|
|
3
|
+
import {ORDER_FIELD_NAME} from '../helpers/constants'
|
|
4
|
+
import initialRank from '../helpers/initialRank'
|
|
5
|
+
import {subscribeSanityClient} from '../desk-structure/globalClientWorkaround'
|
|
6
|
+
|
|
7
|
+
export type SchemaContext = Omit<ConfigContext, 'schema' | 'currentUser' | 'client'>
|
|
8
|
+
|
|
9
|
+
export interface RankFieldConfig {
|
|
10
|
+
type: string
|
|
11
|
+
context: SchemaContext
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const orderRankField = (config: RankFieldConfig) => {
|
|
15
|
+
if (!config?.type || !config.context) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`
|
|
18
|
+
type and context must be provided. context is available when configuring schema.
|
|
19
|
+
Example: orderRankField({type: 'category', context})
|
|
20
|
+
`
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const {type} = config
|
|
25
|
+
|
|
26
|
+
let client: SanityClient | undefined
|
|
27
|
+
subscribeSanityClient(config.context, (updatedClient) => {
|
|
28
|
+
client = updatedClient
|
|
29
|
+
})
|
|
30
|
+
return {
|
|
31
|
+
title: 'Order Rank',
|
|
32
|
+
readOnly: true,
|
|
33
|
+
hidden: true,
|
|
34
|
+
...config,
|
|
35
|
+
name: ORDER_FIELD_NAME,
|
|
36
|
+
type: 'string',
|
|
37
|
+
initialValue: async () => {
|
|
38
|
+
const lastDocOrderRank = await client?.fetch(
|
|
39
|
+
`*[_type == $type]|order(@[$order] desc)[0][$order]`,
|
|
40
|
+
{type, order: ORDER_FIELD_NAME}
|
|
41
|
+
)
|
|
42
|
+
return initialRank(lastDocOrderRank)
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ORDER_FIELD_NAME = `orderRank` as const
|
|
@@ -2,9 +2,9 @@ import {LexoRank} from 'lexorank'
|
|
|
2
2
|
|
|
3
3
|
// Use in initial value field by passing in the rank value of the last document
|
|
4
4
|
// If not value passed, generate a sensibly low rank
|
|
5
|
-
export default function initialRank(lastRankValue = ``) {
|
|
5
|
+
export default function initialRank(lastRankValue = ``): string {
|
|
6
6
|
const lastRank = lastRankValue ? LexoRank.parse(lastRankValue) : LexoRank.min()
|
|
7
7
|
const nextRank = lastRank.genNext().genNext()
|
|
8
8
|
|
|
9
|
-
return nextRank.value
|
|
9
|
+
return (nextRank as any).value
|
|
10
10
|
}
|