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.
@@ -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 {Box, Card, Container, Flex, Grid, Spinner, useTheme} from '@sanity/ui'
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
- newOrder = LexoRank.min().toString()
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
- newOrder =
143
- firstItemOrderRank && typeof firstItemOrderRank === 'string'
144
- ? LexoRank.parse(firstItemOrderRank).genPrev().toString()
145
- : LexoRank.min().toString()
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
- newOrder =
151
- lastItemOrderRank && typeof lastItemOrderRank === 'string'
152
- ? LexoRank.parse(lastItemOrderRank).genNext().toString()
153
- : LexoRank.min().toString()
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
- const itemBeforeRankParsed = itemBeforeRank
159
- ? LexoRank.parse(itemBeforeRank)
160
- : LexoRank.min()
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
- const itemAfterRankParsed = itemAfterRank
164
- ? LexoRank.parse(itemAfterRank)
165
- : LexoRank.max()
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
- move(draggableId, destination, states, newOrder)
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
- // TODO: Render this as a memo/callback
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 {_rev, documentId} = document._metadata || {}
125
+ const {documentId, _rev} = document._metadata || {}
126
126
 
127
- client
127
+ await client
128
128
  .patch(`workflow-metadata.${documentId}`)
129
- .ifRevisionId(_rev as string)
129
+ .ifRevisionId(_rev)
130
130
  .set({state: newStateId, orderRank: newOrder})
131
131
  .commit()
132
- .then(() => {
133
- return toast.push({
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
- return toast.push({
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
@@ -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