sanity-plugin-workflow 1.0.0-beta.1 → 1.0.0-beta.11

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.
Files changed (56) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +81 -13
  3. package/lib/{src/index.d.ts → index.d.ts} +4 -3
  4. package/lib/index.esm.js +2107 -1
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +2120 -1
  7. package/lib/index.js.map +1 -1
  8. package/package.json +51 -40
  9. package/src/actions/AssignWorkflow.tsx +47 -0
  10. package/src/actions/BeginWorkflow.tsx +63 -0
  11. package/src/actions/CompleteWorkflow.tsx +41 -0
  12. package/src/actions/UpdateWorkflow.tsx +126 -0
  13. package/src/badges/AssigneesBadge.tsx +53 -0
  14. package/src/badges/StateBadge.tsx +28 -0
  15. package/src/components/DocumentCard/AvatarGroup.tsx +12 -8
  16. package/src/components/DocumentCard/CompleteButton.tsx +68 -0
  17. package/src/components/DocumentCard/EditButton.tsx +3 -2
  18. package/src/components/DocumentCard/Field.tsx +38 -0
  19. package/src/components/DocumentCard/Validate.tsx +21 -0
  20. package/src/components/DocumentCard/ValidationStatus.tsx +37 -0
  21. package/src/components/DocumentCard/core/DraftStatus.tsx +32 -0
  22. package/src/components/DocumentCard/core/PublishedStatus.tsx +39 -0
  23. package/src/components/DocumentCard/core/TimeAgo.tsx +11 -0
  24. package/src/components/DocumentCard/index.tsx +177 -68
  25. package/src/components/DocumentList.tsx +169 -0
  26. package/src/components/Filters.tsx +168 -0
  27. package/src/components/FloatingCard.tsx +29 -0
  28. package/src/components/StateTitle/Status.tsx +27 -0
  29. package/src/components/StateTitle/index.tsx +78 -0
  30. package/src/components/UserAssignment.tsx +57 -75
  31. package/src/components/UserAssignmentInput.tsx +27 -0
  32. package/src/components/UserDisplay.tsx +57 -0
  33. package/src/components/Verify.tsx +297 -0
  34. package/src/components/WorkflowContext.tsx +71 -0
  35. package/src/components/WorkflowSignal.tsx +30 -0
  36. package/src/components/WorkflowTool.tsx +373 -162
  37. package/src/constants/index.ts +31 -0
  38. package/src/helpers/arraysContainMatchingString.ts +6 -0
  39. package/src/helpers/filterItemsAndSort.ts +41 -0
  40. package/src/helpers/generateMultipleOrderRanks.ts +80 -0
  41. package/src/helpers/initialRank.ts +13 -0
  42. package/src/hooks/useWorkflowDocuments.tsx +76 -78
  43. package/src/hooks/useWorkflowMetadata.tsx +31 -26
  44. package/src/index.ts +60 -57
  45. package/src/schema/workflow/workflow.metadata.ts +68 -0
  46. package/src/tools/index.ts +15 -0
  47. package/src/types/index.ts +27 -6
  48. package/src/actions/DemoteAction.tsx +0 -62
  49. package/src/actions/PromoteAction.tsx +0 -62
  50. package/src/actions/RequestReviewAction.js +0 -61
  51. package/src/actions/index.js +0 -21
  52. package/src/badges/index.tsx +0 -31
  53. package/src/components/Mutate.tsx +0 -54
  54. package/src/components/StateTimeline.tsx +0 -98
  55. package/src/components/UserSelectInput.tsx +0 -43
  56. package/src/schema/workflow/metadata.ts +0 -38
@@ -1,69 +1,56 @@
1
- import React from 'react'
2
1
  import {
3
- Flex,
4
- Card,
2
+ DragDropContext,
3
+ DraggableChildrenFn,
4
+ DragStart,
5
+ Droppable,
6
+ DropResult,
7
+ } from '@hello-pangea/dnd'
8
+ import {
5
9
  Box,
10
+ Card,
11
+ Container,
12
+ Flex,
6
13
  Grid,
7
14
  Spinner,
8
- Label,
9
- useToast,
10
- Container,
11
15
  useTheme,
12
- Button,
16
+ useToast,
13
17
  } from '@sanity/ui'
18
+ import {LexoRank} from 'lexorank'
19
+ import React from 'react'
20
+ import {Tool, useCurrentUser} from 'sanity'
14
21
  import {Feedback, useProjectUsers} from 'sanity-plugin-utils'
15
- import {Tool, useClient} from 'sanity'
16
- import {
17
- DragDropContext,
18
- Droppable,
19
- Draggable,
20
- DropResult,
21
- } from 'react-beautiful-dnd'
22
22
 
23
- import {SanityDocumentWithMetadata, State} from '../types'
24
- import {DocumentCard} from './DocumentCard'
25
- import Mutate from './Mutate'
23
+ import {API_VERSION} from '../constants'
24
+ import {arraysContainMatchingString} from '../helpers/arraysContainMatchingString'
25
+ import {filterItemsAndSort} from '../helpers/filterItemsAndSort'
26
26
  import {useWorkflowDocuments} from '../hooks/useWorkflowDocuments'
27
-
28
- function filterItemsByState(
29
- items: SanityDocumentWithMetadata[],
30
- stateId: string
31
- ) {
32
- return items.filter((item) => item?._metadata?.state === stateId)
33
- }
34
-
35
- type WorkflowToolOptions = {
36
- schemaTypes: string[]
37
- states: State[]
38
- }
27
+ import {State, WorkflowConfig} from '../types'
28
+ import {DocumentCard} from './DocumentCard'
29
+ import DocumentList from './DocumentList'
30
+ import Filters from './Filters'
31
+ import StateTitle from './StateTitle'
32
+ import Verify from './Verify'
39
33
 
40
34
  type WorkflowToolProps = {
41
- tool: Tool<WorkflowToolOptions>
42
- }
43
-
44
- type MutateProps = {
45
- _id: string
46
- _type: string
47
- state: State
48
- documentId: string
35
+ tool: Tool<WorkflowConfig>
49
36
  }
50
37
 
51
38
  export default function WorkflowTool(props: WorkflowToolProps) {
52
39
  const {schemaTypes = [], states = []} = props?.tool?.options ?? {}
53
40
 
54
- const [mutatingDocs, setMutatingDocs] = React.useState<MutateProps[]>([])
55
- const mutationFinished = React.useCallback((documentId: string) => {
56
- setMutatingDocs((docs) => docs.filter((doc) => doc._id !== documentId))
57
- }, [])
58
-
59
- const client = useClient()
60
- const toast = useToast()
61
-
62
41
  const isDarkMode = useTheme().sanity.color.dark
63
42
  const defaultCardTone = isDarkMode ? 'default' : 'transparent'
43
+ const toast = useToast()
44
+
45
+ const userList = useProjectUsers({apiVersion: API_VERSION})
46
+
47
+ const user = useCurrentUser()
48
+ const userRoleNames = user?.roles?.length
49
+ ? user?.roles.map((r) => r.name)
50
+ : []
64
51
 
65
- const userList = useProjectUsers() || []
66
52
  const {workflowData, operations} = useWorkflowDocuments(schemaTypes)
53
+ const [patchingIds, setPatchingIds] = React.useState<string[]>([])
67
54
 
68
55
  // Data to display in cards
69
56
  const {data, loading, error} = workflowData
@@ -71,54 +58,263 @@ export default function WorkflowTool(props: WorkflowToolProps) {
71
58
  // Operations to perform on cards
72
59
  const {move} = operations
73
60
 
74
- const documentsWithoutMetadataIds = data
75
- .filter((doc) => !doc._metadata)
76
- .map((d) => d._id.replace(`drafts.`, ``))
77
-
78
- const importDocuments = React.useCallback(async (ids: string[]) => {
79
- toast.push({
80
- title: 'Importing documents',
81
- status: 'info',
82
- })
83
-
84
- const tx = ids.reduce((item, documentId) => {
85
- return item.createOrReplace({
86
- _id: `workflow-metadata.${documentId}`,
87
- _type: 'workflow.metadata',
88
- state: states[0].id,
89
- documentId,
90
- })
91
- }, client.transaction())
61
+ const [undroppableStates, setUndroppableStates] = React.useState<string[]>([])
62
+ const [draggingFrom, setDraggingFrom] = React.useState(``)
92
63
 
93
- await tx.commit()
64
+ // When drag starts, check for any States we should not allow dropping on
65
+ // Because of either:
66
+ // 1. The "destination" State requires user assignment and the user is not assigned to the dragged document
67
+ // 2. The "source" State State has a list of transitions and the "destination" State is not in that list
68
+ const handleDragStart = React.useCallback(
69
+ (start: DragStart) => {
70
+ const {draggableId, source} = start
71
+ const {droppableId: currentStateId} = source
72
+ setDraggingFrom(currentStateId)
94
73
 
95
- toast.push({
96
- title: 'Imported documents',
97
- status: 'success',
98
- })
99
- }, [])
74
+ const document = data.find(
75
+ (item) => item._metadata?.documentId === draggableId
76
+ )
77
+ const state = states.find((s) => s.id === currentStateId)
78
+
79
+ // This shouldn't happen but TypeScript
80
+ if (!document || !state) return
81
+
82
+ const undroppableStateIds = []
83
+ const statesThatRequireAssignmentIds = states
84
+ .filter((s) => s.requireAssignment)
85
+ .map((s) => s.id)
86
+
87
+ if (statesThatRequireAssignmentIds.length) {
88
+ const documentAssignees = document._metadata?.assignees ?? []
89
+ const userIsAssignedToDocument = user?.id
90
+ ? documentAssignees.includes(user.id)
91
+ : false
92
+
93
+ if (!userIsAssignedToDocument) {
94
+ undroppableStateIds.push(...statesThatRequireAssignmentIds)
95
+ }
96
+ }
97
+
98
+ const statesThatCannotBeTransitionedToIds =
99
+ state.transitions && state.transitions.length
100
+ ? states
101
+ .filter((s) => !state.transitions?.includes(s.id))
102
+ .map((s) => s.id)
103
+ : []
104
+
105
+ if (statesThatCannotBeTransitionedToIds.length) {
106
+ undroppableStateIds.push(...statesThatCannotBeTransitionedToIds)
107
+ }
108
+
109
+ // Remove currentStateId from undroppableStates
110
+ const undroppableExceptSelf = undroppableStateIds.filter(
111
+ (id) => id !== currentStateId
112
+ )
113
+
114
+ if (undroppableExceptSelf.length) {
115
+ setUndroppableStates(undroppableExceptSelf)
116
+ }
117
+ },
118
+ [data, states, user]
119
+ )
100
120
 
101
121
  const handleDragEnd = React.useCallback(
102
- (result: DropResult) => {
122
+ async (result: DropResult) => {
123
+ // Reset undroppable states
124
+ setUndroppableStates([])
125
+ setDraggingFrom(``)
126
+
103
127
  const {draggableId, source, destination} = result
104
- console.log(
105
- `sending ${draggableId} from ${source.droppableId} to ${destination?.droppableId}`
106
- )
107
128
 
108
- if (!destination || destination.droppableId === source.droppableId) {
129
+ if (
130
+ // No destination?
131
+ !destination ||
132
+ // No change in position?
133
+ (destination.droppableId === source.droppableId &&
134
+ destination.index === source.index)
135
+ ) {
109
136
  return
110
137
  }
111
138
 
112
- // The list of mutating docs is how we un/publish documents
113
- const mutatingDoc = move(draggableId, destination, states)
139
+ // Find all items in current state
140
+ const destinationStateItems = [
141
+ ...filterItemsAndSort(data, destination.droppableId, [], null),
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
148
+
149
+ let newOrder
114
150
 
115
- if (mutatingDoc) {
116
- // @ts-ignore
117
- // @todo not sure if these types should be updated. will documentId every be undefined here?
118
- setMutatingDocs((current) => [...current, mutatingDoc])
151
+ if (!destinationStateItems.length) {
152
+ // Only item in state
153
+ // New minimum rank
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
+ }
163
+ } else if (destination.index === 0) {
164
+ // Now first item in order
165
+ const firstItemOrderRank = [...destinationStateItems].shift()?._metadata
166
+ ?.orderRank
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
+ }
179
+ } else if (destination.index + 1 === destinationStateItems.length) {
180
+ // Now last item in order
181
+ const lastItemOrderRank = [...destinationStateItems].pop()?._metadata
182
+ ?.orderRank
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
+ }
195
+ } else {
196
+ // Must be between two items
197
+ const itemBefore = destinationStateItems[destination.index - 1]
198
+ const itemBeforeRank = itemBefore?._metadata?.orderRank
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
+
208
+ const itemAfter = destinationStateItems[destination.index]
209
+ const itemAfterRank = itemAfter?._metadata?.orderRank
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
+ }
218
+
219
+ newOrder = itemBeforeRankParsed.between(itemAfterRankParsed).toString()
119
220
  }
221
+
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))
120
229
  },
121
- [move, states]
230
+ [data, patchingIds, toast, move, states]
231
+ )
232
+
233
+ // Used for the user filter UI
234
+ const uniqueAssignedUsers = React.useMemo(() => {
235
+ const uniqueUserIds = data.reduce((acc, item) => {
236
+ const {assignees = []} = item._metadata ?? {}
237
+ const newAssignees = assignees?.length
238
+ ? assignees.filter((a) => !acc.includes(a))
239
+ : []
240
+ return newAssignees.length ? [...acc, ...newAssignees] : acc
241
+ }, [] as string[])
242
+
243
+ return userList.filter((u) => uniqueUserIds.includes(u.id))
244
+ }, [data, userList])
245
+
246
+ // Selected user IDs filter the visible workflow documents
247
+ const [selectedUserIds, setSelectedUserIds] = React.useState<string[]>(
248
+ uniqueAssignedUsers.map((u) => u.id)
249
+ )
250
+ const toggleSelectedUser = React.useCallback((userId: string) => {
251
+ setSelectedUserIds((prev) =>
252
+ prev.includes(userId)
253
+ ? prev.filter((u) => u !== userId)
254
+ : [...prev, userId]
255
+ )
256
+ }, [])
257
+ const resetSelectedUsers = React.useCallback(() => {
258
+ setSelectedUserIds([])
259
+ }, [])
260
+
261
+ // Selected schema types filter the visible workflow documents
262
+ const [selectedSchemaTypes, setSelectedSchemaTypes] =
263
+ React.useState<string[]>(schemaTypes)
264
+ const toggleSelectedSchemaType = React.useCallback((schemaType: string) => {
265
+ setSelectedSchemaTypes((prev) =>
266
+ prev.includes(schemaType)
267
+ ? prev.filter((u) => u !== schemaType)
268
+ : [...prev, schemaType]
269
+ )
270
+ }, [])
271
+
272
+ // Document IDs that have validation errors
273
+ const [invalidDocumentIds, setInvalidDocumentIds] = React.useState<string[]>(
274
+ []
275
+ )
276
+ const toggleInvalidDocumentId = React.useCallback(
277
+ (docId: string, action: 'ADD' | 'REMOVE') => {
278
+ setInvalidDocumentIds((prev) =>
279
+ action === 'ADD' ? [...prev, docId] : prev.filter((id) => id !== docId)
280
+ )
281
+ },
282
+ []
283
+ )
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]
122
318
  )
123
319
 
124
320
  if (!states?.length) {
@@ -133,96 +329,111 @@ export default function WorkflowTool(props: WorkflowToolProps) {
133
329
  )
134
330
  }
135
331
 
136
- if (error) {
332
+ if (error && !data.length) {
137
333
  return (
138
334
  <Container width={1} padding={5}>
139
- <Feedback tone="critical" title="Error with query" />
335
+ <Feedback
336
+ tone="critical"
337
+ title="Error querying for Workflow documents"
338
+ />
140
339
  </Container>
141
340
  )
142
341
  }
143
342
 
144
343
  return (
145
- <>
146
- {mutatingDocs.length ? (
147
- <div style={{position: `absolute`, bottom: 0, background: 'red'}}>
148
- {mutatingDocs.map((mutate) => (
149
- <Mutate
150
- key={mutate._id}
151
- {...mutate}
152
- onComplete={mutationFinished}
153
- />
154
- ))}
155
- </div>
156
- ) : null}
157
- {documentsWithoutMetadataIds.length > 0 && (
158
- <Box padding={5}>
159
- <Card border padding={3} tone="caution">
160
- <Flex align="center" justify="center">
161
- <Button
162
- onClick={() => importDocuments(documentsWithoutMetadataIds)}
163
- >
164
- Import {documentsWithoutMetadataIds.length} Missing{' '}
165
- {documentsWithoutMetadataIds.length === 1
166
- ? `Document`
167
- : `Documents`}{' '}
168
- into Workflow
169
- </Button>
170
- </Flex>
171
- </Card>
172
- </Box>
173
- )}
174
- <DragDropContext onDragEnd={handleDragEnd}>
344
+ <Flex direction="column" height="fill" overflow="hidden">
345
+ <Verify data={data} userList={userList} states={states} />
346
+
347
+ <Filters
348
+ uniqueAssignedUsers={uniqueAssignedUsers}
349
+ selectedUserIds={selectedUserIds}
350
+ toggleSelectedUser={toggleSelectedUser}
351
+ resetSelectedUsers={resetSelectedUsers}
352
+ schemaTypes={schemaTypes}
353
+ selectedSchemaTypes={selectedSchemaTypes}
354
+ toggleSelectedSchemaType={toggleSelectedSchemaType}
355
+ />
356
+ <DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
175
357
  <Grid columns={states.length} height="fill">
176
- {states.map((state: State, stateIndex: number) => (
177
- <Card key={state.id} borderLeft={stateIndex > 0}>
178
- <Card paddingY={4} padding={3} style={{pointerEvents: `none`}}>
179
- <Label>{state.title}</Label>
180
- </Card>
181
- <Droppable droppableId={state.id}>
182
- {(provided, snapshot) => (
183
- <Card
184
- ref={provided.innerRef}
185
- tone={snapshot.isDraggingOver ? `primary` : defaultCardTone}
186
- height="fill"
187
- >
188
- {loading ? (
189
- <Flex padding={5} align="center" justify="center">
190
- <Spinner muted />
191
- </Flex>
192
- ) : null}
193
-
194
- {data.length > 0 &&
195
- filterItemsByState(data, state.id).map(
196
- (item, itemIndex) => (
197
- // The metadata's documentId is always the published one
198
- <Draggable
199
- key={item?._metadata?.documentId as string}
200
- draggableId={item?._metadata?.documentId as string}
201
- index={itemIndex}
202
- >
203
- {(draggableProvided, draggableSnapshot) => (
204
- <div
205
- ref={draggableProvided.innerRef}
206
- {...draggableProvided.draggableProps}
207
- {...draggableProvided.dragHandleProps}
208
- >
209
- <DocumentCard
210
- isDragging={draggableSnapshot.isDragging}
211
- item={item}
212
- userList={userList}
213
- />
214
- </div>
215
- )}
216
- </Draggable>
217
- )
358
+ {states.map((state: State, stateIndex: number) => {
359
+ const userRoleCanDrop = state?.roles?.length
360
+ ? arraysContainMatchingString(state.roles, userRoleNames)
361
+ : true
362
+ const isDropDisabled =
363
+ !userRoleCanDrop || undroppableStates.includes(state.id)
364
+
365
+ return (
366
+ <Card
367
+ key={state.id}
368
+ borderLeft={stateIndex > 0}
369
+ tone={defaultCardTone}
370
+ >
371
+ <Flex direction="column" height="fill">
372
+ <StateTitle
373
+ state={state}
374
+ requireAssignment={state.requireAssignment ?? false}
375
+ userRoleCanDrop={userRoleCanDrop}
376
+ isDropDisabled={isDropDisabled}
377
+ draggingFrom={draggingFrom}
378
+ documentCount={
379
+ filterItemsAndSort(
380
+ data,
381
+ state.id,
382
+ selectedUserIds,
383
+ selectedSchemaTypes
384
+ ).length
385
+ }
386
+ />
387
+ <Box flex={1}>
388
+ <Droppable
389
+ droppableId={state.id}
390
+ isDropDisabled={isDropDisabled}
391
+ // props required for virtualization
392
+ mode="virtual"
393
+ renderClone={Clone}
394
+ >
395
+ {(provided, snapshot) => (
396
+ <Card
397
+ ref={provided.innerRef}
398
+ tone={
399
+ snapshot.isDraggingOver
400
+ ? `primary`
401
+ : defaultCardTone
402
+ }
403
+ height="fill"
404
+ >
405
+ {loading ? (
406
+ <Flex padding={5} align="center" justify="center">
407
+ <Spinner muted />
408
+ </Flex>
409
+ ) : null}
410
+
411
+ <DocumentList
412
+ data={data}
413
+ invalidDocumentIds={invalidDocumentIds}
414
+ patchingIds={patchingIds}
415
+ selectedSchemaTypes={selectedSchemaTypes}
416
+ selectedUserIds={selectedUserIds}
417
+ state={state}
418
+ states={states}
419
+ toggleInvalidDocumentId={toggleInvalidDocumentId}
420
+ user={user}
421
+ userList={userList}
422
+ userRoleCanDrop={userRoleCanDrop}
423
+ />
424
+
425
+ {/* Not required for virtualized lists */}
426
+ {/* {provided.placeholder} */}
427
+ </Card>
218
428
  )}
219
- </Card>
220
- )}
221
- </Droppable>
222
- </Card>
223
- ))}
429
+ </Droppable>
430
+ </Box>
431
+ </Flex>
432
+ </Card>
433
+ )
434
+ })}
224
435
  </Grid>
225
436
  </DragDropContext>
226
- </>
437
+ </Flex>
227
438
  )
228
439
  }
@@ -0,0 +1,31 @@
1
+ import {defineStates, WorkflowConfig} from '../types'
2
+
3
+ export const API_VERSION = `2023-01-01`
4
+
5
+ export const DEFAULT_CONFIG: Required<WorkflowConfig> = {
6
+ schemaTypes: [],
7
+ states: defineStates([
8
+ {
9
+ id: 'inReview',
10
+ title: 'In review',
11
+ color: 'primary',
12
+ roles: ['editor', 'administrator'],
13
+ transitions: ['changesRequested', 'approved'],
14
+ },
15
+ {
16
+ id: 'changesRequested',
17
+ title: 'Changes requested',
18
+ color: 'warning',
19
+ roles: ['editor', 'administrator'],
20
+ transitions: ['approved'],
21
+ },
22
+ {
23
+ id: 'approved',
24
+ title: 'Approved',
25
+ color: 'success',
26
+ roles: ['administrator'],
27
+ transitions: ['changesRequested'],
28
+ requireAssignment: true,
29
+ },
30
+ ]),
31
+ }
@@ -0,0 +1,6 @@
1
+ export function arraysContainMatchingString(
2
+ one: string[],
3
+ two: string[]
4
+ ): boolean {
5
+ return one.some((item) => two.includes(item))
6
+ }
@@ -0,0 +1,41 @@
1
+ import {SanityDocumentWithMetadata} from '../types'
2
+
3
+ export function filterItemsAndSort(
4
+ items: SanityDocumentWithMetadata[],
5
+ stateId: string,
6
+ selectedUsers: string[] = [],
7
+ selectedSchemaTypes: null | string[] = []
8
+ ): SanityDocumentWithMetadata[] {
9
+ return (
10
+ items
11
+ // Only items that have existing documents
12
+ .filter((item) => item?._id)
13
+ // Only items of this state
14
+ .filter((item) => item?._metadata?.state === stateId)
15
+ // Only items with selected users, if the document has any assigned users
16
+ .filter((item) =>
17
+ selectedUsers.length && item._metadata?.assignees?.length
18
+ ? item._metadata?.assignees.some((assignee) =>
19
+ selectedUsers.includes(assignee)
20
+ )
21
+ : !selectedUsers.length
22
+ )
23
+ // Only items of selected schema types, if any are selected
24
+ .filter((item) => {
25
+ if (!selectedSchemaTypes) {
26
+ return true
27
+ }
28
+
29
+ return selectedSchemaTypes.length
30
+ ? selectedSchemaTypes.includes(item._type)
31
+ : false
32
+ })
33
+ // Sort by metadata orderRank, a string field
34
+ .sort((a, b) => {
35
+ const aOrderRank = a._metadata?.orderRank || '0'
36
+ const bOrderRank = b._metadata?.orderRank || '0'
37
+
38
+ return aOrderRank.localeCompare(bOrderRank)
39
+ })
40
+ )
41
+ }