@sanity/orderable-document-list 0.0.10 → 1.0.0-v3-studio.2
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 +135 -55
- package/lib/index.d.ts +31 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +827 -30
- package/lib/index.js.map +1 -1
- package/lib/index.modern.js +818 -0
- package/lib/index.modern.js.map +1 -0
- package/package.json +57 -23
- 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/{orderableDocumentListDeskItem.js → orderableDocumentListDeskItem.ts} +24 -10
- package/src/fields/{orderRankField.js → orderRankField.ts} +12 -10
- package/src/fields/{orderRankOrdering.js → orderRankOrdering.ts} +0 -0
- package/src/helpers/client.ts +5 -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/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
|
/>
|
package/src/desk-structure/{orderableDocumentListDeskItem.js → orderableDocumentListDeskItem.ts}
RENAMED
|
@@ -1,26 +1,40 @@
|
|
|
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'
|
|
6
7
|
|
|
7
|
-
export
|
|
8
|
-
|
|
8
|
+
export interface OrderableListConfig {
|
|
9
|
+
type: string
|
|
10
|
+
id?: string
|
|
11
|
+
title?: string
|
|
12
|
+
icon?: ComponentType
|
|
13
|
+
params?: Record<string, unknown>
|
|
14
|
+
filter?: string
|
|
15
|
+
context: ConfigContext
|
|
16
|
+
S: StructureBuilder
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function orderableDocumentListDeskItem(config: OrderableListConfig) {
|
|
20
|
+
if (!config?.type || !config.context || !config.S) {
|
|
9
21
|
throw new Error(`
|
|
10
|
-
|
|
11
|
-
|
|
22
|
+
type, context and S (StructureBuilder) must be provided.
|
|
23
|
+
context and S are available when configuring structure.
|
|
12
24
|
Example: orderableDocumentListDeskItem({type: 'category'})
|
|
13
25
|
`)
|
|
14
26
|
}
|
|
15
27
|
|
|
16
|
-
const {type, filter, params, title, icon, id} = config
|
|
28
|
+
const {type, filter, params, title, icon, id, context, S} = config
|
|
29
|
+
const {schema, getClient} = context
|
|
30
|
+
const client = getClient({apiVersion: '2021-09-01'})
|
|
17
31
|
|
|
18
32
|
const listTitle = title ?? `Orderable ${type}`
|
|
19
33
|
const listId = id ?? `orderable-${type}`
|
|
20
34
|
const listIcon = icon ?? SortIcon
|
|
21
35
|
const typeTitle = schema.get(type)?.title ?? type
|
|
22
36
|
|
|
23
|
-
return S.listItem(
|
|
37
|
+
return S.listItem()
|
|
24
38
|
.title(listTitle)
|
|
25
39
|
.id(listId)
|
|
26
40
|
.icon(listIcon)
|
|
@@ -33,7 +47,7 @@ export function orderableDocumentListDeskItem(config = {}) {
|
|
|
33
47
|
|
|
34
48
|
type: 'component',
|
|
35
49
|
component: OrderableDocumentList,
|
|
36
|
-
options: {type, filter, params},
|
|
50
|
+
options: {type, filter, params, client},
|
|
37
51
|
menuItems: [
|
|
38
52
|
S.menuItem()
|
|
39
53
|
.title(`Create new ${typeTitle}`)
|
|
@@ -1,35 +1,37 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {type ConfigContext, defineField} from 'sanity'
|
|
2
2
|
import {ORDER_FIELD_NAME} from '../helpers/constants'
|
|
3
3
|
import initialRank from '../helpers/initialRank'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
export type SchemaContext = Omit<ConfigContext, 'schema' | 'currentUser' | 'client'>
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export interface RankFieldConfig {
|
|
8
|
+
type: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const orderRankField = (config: RankFieldConfig) => {
|
|
8
12
|
if (!config?.type) {
|
|
9
13
|
throw new Error(
|
|
10
14
|
`
|
|
11
|
-
|
|
15
|
+
type must be provided.
|
|
12
16
|
Example: orderRankField({type: 'category'})
|
|
13
17
|
`
|
|
14
18
|
)
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
const {type} = config
|
|
18
|
-
|
|
19
|
-
return {
|
|
22
|
+
return defineField({
|
|
20
23
|
title: 'Order Rank',
|
|
21
24
|
readOnly: true,
|
|
22
25
|
hidden: true,
|
|
23
26
|
...config,
|
|
24
27
|
name: ORDER_FIELD_NAME,
|
|
25
28
|
type: 'string',
|
|
26
|
-
initialValue: async () => {
|
|
27
|
-
const lastDocOrderRank = await
|
|
29
|
+
initialValue: async ({getClient}) => {
|
|
30
|
+
const lastDocOrderRank = await getClient({apiVersion: '2021-09-01'}).fetch(
|
|
28
31
|
`*[_type == $type]|order(@[$order] desc)[0][$order]`,
|
|
29
32
|
{type, order: ORDER_FIELD_NAME}
|
|
30
33
|
)
|
|
31
|
-
|
|
32
34
|
return initialRank(lastDocOrderRank)
|
|
33
35
|
},
|
|
34
|
-
}
|
|
36
|
+
})
|
|
35
37
|
}
|
|
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
|
}
|
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import {LexoRank} from 'lexorank'
|
|
2
|
+
import type {PatchOperations, SanityDocument} from 'sanity'
|
|
2
3
|
import {ORDER_FIELD_NAME} from './constants'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
export interface MaifestArgs {
|
|
6
|
+
entities: SanityDocument[]
|
|
7
|
+
selectedItems: SanityDocument[]
|
|
8
|
+
isMovingUp: boolean
|
|
9
|
+
curIndex: number
|
|
10
|
+
nextIndex: number
|
|
11
|
+
prevIndex: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ReorderArgs {
|
|
15
|
+
entities: SanityDocument[]
|
|
16
|
+
selectedIds: string[]
|
|
17
|
+
source: any
|
|
18
|
+
destination: any
|
|
19
|
+
debug?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function lexicographicalSort(a: {[ORDER_FIELD_NAME]: string}, b: {[ORDER_FIELD_NAME]: string}) {
|
|
5
23
|
if (a[ORDER_FIELD_NAME] < b[ORDER_FIELD_NAME]) {
|
|
6
24
|
return -1
|
|
7
25
|
}
|
|
@@ -11,42 +29,13 @@ function lexicographicalSort(a, b) {
|
|
|
11
29
|
return 0
|
|
12
30
|
}
|
|
13
31
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const table = [
|
|
22
|
-
{
|
|
23
|
-
name: `Before`,
|
|
24
|
-
title:
|
|
25
|
-
curIndex === 0 ? `<<Start of List>>` : entities[isMovingUp ? prevIndex : curIndex]?.title,
|
|
26
|
-
order: curIndex === 0 ? `000` : entities[isMovingUp ? prevIndex : curIndex][ORDER_FIELD_NAME],
|
|
27
|
-
},
|
|
28
|
-
...selectedItems.map((item, itemIndex) => ({
|
|
29
|
-
name: itemIndex,
|
|
30
|
-
title: item?.title,
|
|
31
|
-
order: item[ORDER_FIELD_NAME],
|
|
32
|
-
})),
|
|
33
|
-
{
|
|
34
|
-
name: `After`,
|
|
35
|
-
title:
|
|
36
|
-
curIndex === entities.length - 1
|
|
37
|
-
? `<<End of List>>`
|
|
38
|
-
: entities[isMovingUp ? curIndex : nextIndex]?.title,
|
|
39
|
-
order:
|
|
40
|
-
curIndex === entities.length - 1
|
|
41
|
-
? `zzz`
|
|
42
|
-
: entities[isMovingUp ? curIndex : nextIndex][ORDER_FIELD_NAME],
|
|
43
|
-
},
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
return table.sort(lexicographicalSort)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const reorderDocuments = ({entities, selectedIds, source, destination, debug = false}) => {
|
|
32
|
+
export const reorderDocuments = ({
|
|
33
|
+
entities,
|
|
34
|
+
selectedIds,
|
|
35
|
+
source,
|
|
36
|
+
destination,
|
|
37
|
+
debug = false,
|
|
38
|
+
}: ReorderArgs) => {
|
|
50
39
|
const startIndex = source.index
|
|
51
40
|
const endIndex = destination.index
|
|
52
41
|
const isMovingUp = startIndex > endIndex
|
|
@@ -70,21 +59,21 @@ export const reorderDocuments = ({entities, selectedIds, source, destination, de
|
|
|
70
59
|
if (curIndex === endIndex) {
|
|
71
60
|
const prevIndex = curIndex - 1
|
|
72
61
|
const prevRank = entities[prevIndex]?.[ORDER_FIELD_NAME]
|
|
73
|
-
? LexoRank.parse(entities[prevIndex]?.[ORDER_FIELD_NAME])
|
|
62
|
+
? LexoRank.parse(entities[prevIndex]?.[ORDER_FIELD_NAME] as string)
|
|
74
63
|
: LexoRank.min()
|
|
75
64
|
|
|
76
|
-
const curRank = LexoRank.parse(entities[curIndex][ORDER_FIELD_NAME])
|
|
65
|
+
const curRank = LexoRank.parse(entities[curIndex][ORDER_FIELD_NAME] as string)
|
|
77
66
|
|
|
78
67
|
const nextIndex = curIndex + 1
|
|
79
68
|
const nextRank = entities[nextIndex]?.[ORDER_FIELD_NAME]
|
|
80
|
-
? LexoRank.parse(entities[nextIndex]?.[ORDER_FIELD_NAME])
|
|
69
|
+
? LexoRank.parse(entities[nextIndex]?.[ORDER_FIELD_NAME] as string)
|
|
81
70
|
: LexoRank.max()
|
|
82
71
|
|
|
83
72
|
let betweenRank = isMovingUp ? prevRank.between(curRank) : curRank.between(nextRank)
|
|
84
73
|
|
|
85
74
|
// For each selected item, assign a new orderRank between now and next
|
|
86
75
|
for (let selectedIndex = 0; selectedIndex < selectedItems.length; selectedIndex += 1) {
|
|
87
|
-
selectedItems[selectedIndex][ORDER_FIELD_NAME] = betweenRank.value
|
|
76
|
+
selectedItems[selectedIndex][ORDER_FIELD_NAME] = (betweenRank as any).value as string
|
|
88
77
|
betweenRank = isMovingUp ? betweenRank.between(curRank) : betweenRank.between(nextRank)
|
|
89
78
|
}
|
|
90
79
|
|
|
@@ -100,10 +89,10 @@ export const reorderDocuments = ({entities, selectedIds, source, destination, de
|
|
|
100
89
|
|
|
101
90
|
return {all: [...acc.all, cur], selected: acc.selected}
|
|
102
91
|
},
|
|
103
|
-
{all: [], selected: []}
|
|
92
|
+
{all: [] as SanityDocument[], selected: [] as SanityDocument[]}
|
|
104
93
|
)
|
|
105
94
|
|
|
106
|
-
const patches = selected.map((doc) => {
|
|
95
|
+
const patches: [string, PatchOperations][] = selected.map((doc) => {
|
|
107
96
|
return [
|
|
108
97
|
doc._id,
|
|
109
98
|
{
|
|
@@ -115,7 +104,7 @@ export const reorderDocuments = ({entities, selectedIds, source, destination, de
|
|
|
115
104
|
})
|
|
116
105
|
|
|
117
106
|
// Safety-check to make sure everything is in order
|
|
118
|
-
const allSorted = all.sort(lexicographicalSort)
|
|
107
|
+
const allSorted = (all as unknown as {[ORDER_FIELD_NAME]: string}[]).sort(lexicographicalSort)
|
|
119
108
|
|
|
120
109
|
return {newOrder: allSorted, patches, message}
|
|
121
110
|
}
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import {LexoRank} from 'lexorank'
|
|
2
|
-
import sanityClient from 'part:@sanity/base/client'
|
|
3
2
|
import {ORDER_FIELD_NAME} from './constants'
|
|
4
|
-
|
|
5
|
-
const client = sanityClient.withConfig({
|
|
6
|
-
apiVersion: '2021-09-01',
|
|
7
|
-
})
|
|
8
|
-
|
|
3
|
+
import {SanityClient} from '@sanity/client'
|
|
9
4
|
// Function to wipe and re-do ordering with LexoRank
|
|
10
5
|
// Will at least attempt to start with the current order
|
|
11
|
-
export async function resetOrder(type =
|
|
6
|
+
export async function resetOrder(type = ``, client: SanityClient) {
|
|
12
7
|
const query = `*[_type == $type]|order(@[$order] asc)._id`
|
|
13
8
|
const queryParams = {type, order: ORDER_FIELD_NAME}
|
|
14
9
|
const documents = await client.fetch(query, queryParams)
|
|
@@ -25,7 +20,7 @@ export async function resetOrder(type = ``) {
|
|
|
25
20
|
aLexoRank = aLexoRank.genNext().genNext()
|
|
26
21
|
|
|
27
22
|
transaction.patch(documents[index], {
|
|
28
|
-
set: {[ORDER_FIELD_NAME]: aLexoRank.value},
|
|
23
|
+
set: {[ORDER_FIELD_NAME]: (aLexoRank as any).value as string},
|
|
29
24
|
})
|
|
30
25
|
}
|
|
31
26
|
|