sanity-plugin-workflow 1.0.0-beta.5 → 1.0.0-beta.7
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/README.md +9 -5
- package/lib/index.esm.js +171 -71
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +171 -71
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/DocumentCard/CompleteButton.tsx +47 -32
- package/src/components/DocumentCard/index.tsx +20 -42
- package/src/components/DocumentList.tsx +5 -2
- package/src/components/Filters.tsx +1 -1
- package/src/components/StateTitle/index.tsx +31 -26
- package/src/components/Verify.tsx +36 -4
- package/src/components/WorkflowTool.tsx +122 -51
- package/src/hooks/useWorkflowDocuments.tsx +11 -8
- package/src/types/index.ts +0 -3
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DragDropContext,
|
|
3
|
+
DraggableChildrenFn,
|
|
3
4
|
DragStart,
|
|
4
5
|
Droppable,
|
|
5
6
|
DropResult,
|
|
6
7
|
} from '@hello-pangea/dnd'
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Box,
|
|
10
|
+
Card,
|
|
11
|
+
Container,
|
|
12
|
+
Flex,
|
|
13
|
+
Grid,
|
|
14
|
+
Spinner,
|
|
15
|
+
useTheme,
|
|
16
|
+
useToast,
|
|
17
|
+
} from '@sanity/ui'
|
|
8
18
|
import {LexoRank} from 'lexorank'
|
|
9
19
|
import React from 'react'
|
|
10
20
|
import {Tool, useCurrentUser} from 'sanity'
|
|
@@ -30,6 +40,7 @@ export default function WorkflowTool(props: WorkflowToolProps) {
|
|
|
30
40
|
|
|
31
41
|
const isDarkMode = useTheme().sanity.color.dark
|
|
32
42
|
const defaultCardTone = isDarkMode ? 'default' : 'transparent'
|
|
43
|
+
const toast = useToast()
|
|
33
44
|
|
|
34
45
|
const userList = useProjectUsers({apiVersion: API_VERSION})
|
|
35
46
|
|
|
@@ -39,6 +50,7 @@ export default function WorkflowTool(props: WorkflowToolProps) {
|
|
|
39
50
|
: []
|
|
40
51
|
|
|
41
52
|
const {workflowData, operations} = useWorkflowDocuments(schemaTypes)
|
|
53
|
+
const [patchingIds, setPatchingIds] = React.useState<string[]>([])
|
|
42
54
|
|
|
43
55
|
// Data to display in cards
|
|
44
56
|
const {data, loading, error} = workflowData
|
|
@@ -107,7 +119,7 @@ export default function WorkflowTool(props: WorkflowToolProps) {
|
|
|
107
119
|
)
|
|
108
120
|
|
|
109
121
|
const handleDragEnd = React.useCallback(
|
|
110
|
-
(result: DropResult) => {
|
|
122
|
+
async (result: DropResult) => {
|
|
111
123
|
// Reset undroppable states
|
|
112
124
|
setUndroppableStates([])
|
|
113
125
|
setDraggingFrom(``)
|
|
@@ -128,48 +140,94 @@ export default function WorkflowTool(props: WorkflowToolProps) {
|
|
|
128
140
|
const destinationStateItems = [
|
|
129
141
|
...filterItemsAndSort(data, destination.droppableId, [], null),
|
|
130
142
|
]
|
|
143
|
+
const destinationStateIndex = states.findIndex(
|
|
144
|
+
(s) => s.id === destination.droppableId
|
|
145
|
+
)
|
|
146
|
+
const globalStateMinimumRank = data[0]._metadata.orderRank
|
|
147
|
+
const globalStateMaximumRank = data[data.length - 1]._metadata.orderRank
|
|
131
148
|
|
|
132
149
|
let newOrder
|
|
133
150
|
|
|
134
151
|
if (!destinationStateItems.length) {
|
|
135
152
|
// Only item in state
|
|
136
153
|
// New minimum rank
|
|
137
|
-
|
|
154
|
+
if (destinationStateIndex === 0) {
|
|
155
|
+
// Only the first state should generate an absolute minimum rank
|
|
156
|
+
newOrder = LexoRank.min().toString()
|
|
157
|
+
} else {
|
|
158
|
+
// Otherwise create the next rank between min and the globally minimum rank
|
|
159
|
+
newOrder = LexoRank.parse(globalStateMinimumRank)
|
|
160
|
+
.between(LexoRank.min())
|
|
161
|
+
.toString()
|
|
162
|
+
}
|
|
138
163
|
} else if (destination.index === 0) {
|
|
139
164
|
// Now first item in order
|
|
140
165
|
const firstItemOrderRank = [...destinationStateItems].shift()?._metadata
|
|
141
166
|
?.orderRank
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
167
|
+
|
|
168
|
+
if (firstItemOrderRank && typeof firstItemOrderRank === 'string') {
|
|
169
|
+
newOrder = LexoRank.parse(firstItemOrderRank).genPrev().toString()
|
|
170
|
+
} else if (destinationStateIndex === 0) {
|
|
171
|
+
// Only the first state should generate an absolute minimum rank
|
|
172
|
+
newOrder = LexoRank.min().toString()
|
|
173
|
+
} else {
|
|
174
|
+
// Otherwise create the next rank between min and the globally minimum rank
|
|
175
|
+
newOrder = LexoRank.parse(globalStateMinimumRank)
|
|
176
|
+
.between(LexoRank.min())
|
|
177
|
+
.toString()
|
|
178
|
+
}
|
|
146
179
|
} else if (destination.index + 1 === destinationStateItems.length) {
|
|
147
180
|
// Now last item in order
|
|
148
181
|
const lastItemOrderRank = [...destinationStateItems].pop()?._metadata
|
|
149
182
|
?.orderRank
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
183
|
+
|
|
184
|
+
if (lastItemOrderRank && typeof lastItemOrderRank === 'string') {
|
|
185
|
+
newOrder = LexoRank.parse(lastItemOrderRank).genNext().toString()
|
|
186
|
+
} else if (destinationStateIndex === states.length - 1) {
|
|
187
|
+
// Only the last state should generate an absolute maximum rank
|
|
188
|
+
newOrder = LexoRank.max().toString()
|
|
189
|
+
} else {
|
|
190
|
+
// Otherwise create the next rank between max and the globally maximum rank
|
|
191
|
+
newOrder = LexoRank.parse(globalStateMaximumRank)
|
|
192
|
+
.between(LexoRank.min())
|
|
193
|
+
.toString()
|
|
194
|
+
}
|
|
154
195
|
} else {
|
|
155
196
|
// Must be between two items
|
|
156
197
|
const itemBefore = destinationStateItems[destination.index - 1]
|
|
157
198
|
const itemBeforeRank = itemBefore?._metadata?.orderRank
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
199
|
+
let itemBeforeRankParsed
|
|
200
|
+
if (itemBeforeRank) {
|
|
201
|
+
itemBeforeRankParsed = LexoRank.parse(itemBeforeRank)
|
|
202
|
+
} else if (destinationStateIndex === 0) {
|
|
203
|
+
itemBeforeRankParsed = LexoRank.min()
|
|
204
|
+
} else {
|
|
205
|
+
itemBeforeRankParsed = LexoRank.parse(globalStateMinimumRank)
|
|
206
|
+
}
|
|
207
|
+
|
|
161
208
|
const itemAfter = destinationStateItems[destination.index]
|
|
162
209
|
const itemAfterRank = itemAfter?._metadata?.orderRank
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
210
|
+
let itemAfterRankParsed
|
|
211
|
+
if (itemAfterRank) {
|
|
212
|
+
itemAfterRankParsed = LexoRank.parse(itemAfterRank)
|
|
213
|
+
} else if (destinationStateIndex === states.length - 1) {
|
|
214
|
+
itemAfterRankParsed = LexoRank.max()
|
|
215
|
+
} else {
|
|
216
|
+
itemAfterRankParsed = LexoRank.parse(globalStateMaximumRank)
|
|
217
|
+
}
|
|
166
218
|
|
|
167
219
|
newOrder = itemBeforeRankParsed.between(itemAfterRankParsed).toString()
|
|
168
220
|
}
|
|
169
221
|
|
|
170
|
-
|
|
222
|
+
setPatchingIds([...patchingIds, draggableId])
|
|
223
|
+
toast.push({
|
|
224
|
+
status: 'info',
|
|
225
|
+
title: 'Updating document state...',
|
|
226
|
+
})
|
|
227
|
+
await move(draggableId, destination, states, newOrder)
|
|
228
|
+
setPatchingIds((ids: string[]) => ids.filter((id) => id !== draggableId))
|
|
171
229
|
},
|
|
172
|
-
[data, move, states]
|
|
230
|
+
[data, patchingIds, toast, move, states]
|
|
173
231
|
)
|
|
174
232
|
|
|
175
233
|
// Used for the user filter UI
|
|
@@ -224,6 +282,41 @@ export default function WorkflowTool(props: WorkflowToolProps) {
|
|
|
224
282
|
[]
|
|
225
283
|
)
|
|
226
284
|
|
|
285
|
+
const Clone: DraggableChildrenFn = React.useCallback(
|
|
286
|
+
(provided, snapshot, rubric) => {
|
|
287
|
+
const item = data.find(
|
|
288
|
+
(doc) => doc?._metadata?.documentId === rubric.draggableId
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div
|
|
293
|
+
{...provided.draggableProps}
|
|
294
|
+
{...provided.dragHandleProps}
|
|
295
|
+
ref={provided.innerRef}
|
|
296
|
+
>
|
|
297
|
+
{item ? (
|
|
298
|
+
<DocumentCard
|
|
299
|
+
// Assumed false, if it's dragging it's not disabled
|
|
300
|
+
isDragDisabled={false}
|
|
301
|
+
// Assumed false, if it's dragging it's not patching
|
|
302
|
+
isPatching={false}
|
|
303
|
+
// Assumed true, if you can drag it you can drop it
|
|
304
|
+
userRoleCanDrop
|
|
305
|
+
isDragging={snapshot.isDragging}
|
|
306
|
+
item={item}
|
|
307
|
+
states={states}
|
|
308
|
+
toggleInvalidDocumentId={toggleInvalidDocumentId}
|
|
309
|
+
userList={userList}
|
|
310
|
+
/>
|
|
311
|
+
) : (
|
|
312
|
+
<Feedback title="Item not found" tone="caution" />
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
)
|
|
316
|
+
},
|
|
317
|
+
[data, states, toggleInvalidDocumentId, userList]
|
|
318
|
+
)
|
|
319
|
+
|
|
227
320
|
if (!states?.length) {
|
|
228
321
|
return (
|
|
229
322
|
<Container width={1} padding={5}>
|
|
@@ -280,9 +373,16 @@ export default function WorkflowTool(props: WorkflowToolProps) {
|
|
|
280
373
|
state={state}
|
|
281
374
|
requireAssignment={state.requireAssignment ?? false}
|
|
282
375
|
userRoleCanDrop={userRoleCanDrop}
|
|
283
|
-
// operation={state.operation}
|
|
284
376
|
isDropDisabled={isDropDisabled}
|
|
285
377
|
draggingFrom={draggingFrom}
|
|
378
|
+
documentCount={
|
|
379
|
+
filterItemsAndSort(
|
|
380
|
+
data,
|
|
381
|
+
state.id,
|
|
382
|
+
selectedUserIds,
|
|
383
|
+
selectedSchemaTypes
|
|
384
|
+
).length
|
|
385
|
+
}
|
|
286
386
|
/>
|
|
287
387
|
<Box flex={1}>
|
|
288
388
|
<Droppable
|
|
@@ -290,37 +390,7 @@ export default function WorkflowTool(props: WorkflowToolProps) {
|
|
|
290
390
|
isDropDisabled={isDropDisabled}
|
|
291
391
|
// props required for virtualization
|
|
292
392
|
mode="virtual"
|
|
293
|
-
|
|
294
|
-
renderClone={(provided, snapshot, rubric) => {
|
|
295
|
-
const item = data.find(
|
|
296
|
-
(doc) =>
|
|
297
|
-
doc?._metadata?.documentId === rubric.draggableId
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
return (
|
|
301
|
-
<div
|
|
302
|
-
{...provided.draggableProps}
|
|
303
|
-
{...provided.dragHandleProps}
|
|
304
|
-
ref={provided.innerRef}
|
|
305
|
-
>
|
|
306
|
-
{item ? (
|
|
307
|
-
<DocumentCard
|
|
308
|
-
isDragDisabled={false}
|
|
309
|
-
userRoleCanDrop={userRoleCanDrop}
|
|
310
|
-
isDragging={snapshot.isDragging}
|
|
311
|
-
item={item}
|
|
312
|
-
states={states}
|
|
313
|
-
toggleInvalidDocumentId={
|
|
314
|
-
toggleInvalidDocumentId
|
|
315
|
-
}
|
|
316
|
-
userList={userList}
|
|
317
|
-
/>
|
|
318
|
-
) : (
|
|
319
|
-
<Feedback title="Item not found" tone="caution" />
|
|
320
|
-
)}
|
|
321
|
-
</div>
|
|
322
|
-
)
|
|
323
|
-
}}
|
|
393
|
+
renderClone={Clone}
|
|
324
394
|
>
|
|
325
395
|
{(provided, snapshot) => (
|
|
326
396
|
<Card
|
|
@@ -342,6 +412,7 @@ export default function WorkflowTool(props: WorkflowToolProps) {
|
|
|
342
412
|
<DocumentList
|
|
343
413
|
data={data}
|
|
344
414
|
invalidDocumentIds={invalidDocumentIds}
|
|
415
|
+
patchingIds={patchingIds}
|
|
345
416
|
selectedSchemaTypes={selectedSchemaTypes}
|
|
346
417
|
selectedUserIds={selectedUserIds}
|
|
347
418
|
state={state}
|
|
@@ -65,7 +65,7 @@ export function useWorkflowDocuments(schemaTypes: string[]): WorkflowDocuments {
|
|
|
65
65
|
}, [data])
|
|
66
66
|
|
|
67
67
|
const move = React.useCallback(
|
|
68
|
-
(
|
|
68
|
+
async (
|
|
69
69
|
draggedId: string,
|
|
70
70
|
destination: DraggableLocation,
|
|
71
71
|
states: State[],
|
|
@@ -122,27 +122,30 @@ export function useWorkflowDocuments(schemaTypes: string[]): WorkflowDocuments {
|
|
|
122
122
|
const {_id, _type} = document
|
|
123
123
|
|
|
124
124
|
// Metadata + useDocumentOperation always uses Published id
|
|
125
|
-
const {
|
|
125
|
+
const {documentId, _rev} = document._metadata || {}
|
|
126
126
|
|
|
127
|
-
client
|
|
127
|
+
await client
|
|
128
128
|
.patch(`workflow-metadata.${documentId}`)
|
|
129
|
-
.ifRevisionId(_rev
|
|
129
|
+
.ifRevisionId(_rev)
|
|
130
130
|
.set({state: newStateId, orderRank: newOrder})
|
|
131
131
|
.commit()
|
|
132
|
-
.then(() => {
|
|
133
|
-
|
|
132
|
+
.then((res) => {
|
|
133
|
+
toast.push({
|
|
134
134
|
title: `Moved to "${newState?.title ?? newStateId}"`,
|
|
135
135
|
status: 'success',
|
|
136
136
|
})
|
|
137
|
+
return res
|
|
137
138
|
})
|
|
138
|
-
.catch(() => {
|
|
139
|
+
.catch((err) => {
|
|
139
140
|
// Revert optimistic update
|
|
140
141
|
setLocalDocuments(currentLocalData)
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
toast.push({
|
|
143
144
|
title: `Failed to move to "${newState?.title ?? newStateId}"`,
|
|
145
|
+
description: err.message,
|
|
144
146
|
status: 'error',
|
|
145
147
|
})
|
|
148
|
+
return null
|
|
146
149
|
})
|
|
147
150
|
|
|
148
151
|
// Send back to the workflow board so a document update can happen
|
package/src/types/index.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import {SanityDocumentLike} from 'sanity'
|
|
2
2
|
|
|
3
|
-
// export type Operation = 'publish' | 'unpublish'
|
|
4
|
-
|
|
5
3
|
export type State = {
|
|
6
4
|
id: string
|
|
7
5
|
transitions: string[]
|
|
8
6
|
title: string
|
|
9
|
-
// operation?: Operation
|
|
10
7
|
roles?: string[]
|
|
11
8
|
requireAssignment?: boolean
|
|
12
9
|
requireValidation?: boolean
|