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.
- package/LICENSE +1 -1
- package/README.md +81 -13
- package/lib/{src/index.d.ts → index.d.ts} +4 -3
- package/lib/index.esm.js +2107 -1
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +2120 -1
- package/lib/index.js.map +1 -1
- package/package.json +51 -40
- package/src/actions/AssignWorkflow.tsx +47 -0
- package/src/actions/BeginWorkflow.tsx +63 -0
- package/src/actions/CompleteWorkflow.tsx +41 -0
- package/src/actions/UpdateWorkflow.tsx +126 -0
- package/src/badges/AssigneesBadge.tsx +53 -0
- package/src/badges/StateBadge.tsx +28 -0
- package/src/components/DocumentCard/AvatarGroup.tsx +12 -8
- package/src/components/DocumentCard/CompleteButton.tsx +68 -0
- package/src/components/DocumentCard/EditButton.tsx +3 -2
- package/src/components/DocumentCard/Field.tsx +38 -0
- package/src/components/DocumentCard/Validate.tsx +21 -0
- package/src/components/DocumentCard/ValidationStatus.tsx +37 -0
- package/src/components/DocumentCard/core/DraftStatus.tsx +32 -0
- package/src/components/DocumentCard/core/PublishedStatus.tsx +39 -0
- package/src/components/DocumentCard/core/TimeAgo.tsx +11 -0
- package/src/components/DocumentCard/index.tsx +177 -68
- package/src/components/DocumentList.tsx +169 -0
- package/src/components/Filters.tsx +168 -0
- package/src/components/FloatingCard.tsx +29 -0
- package/src/components/StateTitle/Status.tsx +27 -0
- package/src/components/StateTitle/index.tsx +78 -0
- package/src/components/UserAssignment.tsx +57 -75
- package/src/components/UserAssignmentInput.tsx +27 -0
- package/src/components/UserDisplay.tsx +57 -0
- package/src/components/Verify.tsx +297 -0
- package/src/components/WorkflowContext.tsx +71 -0
- package/src/components/WorkflowSignal.tsx +30 -0
- package/src/components/WorkflowTool.tsx +373 -162
- package/src/constants/index.ts +31 -0
- package/src/helpers/arraysContainMatchingString.ts +6 -0
- package/src/helpers/filterItemsAndSort.ts +41 -0
- package/src/helpers/generateMultipleOrderRanks.ts +80 -0
- package/src/helpers/initialRank.ts +13 -0
- package/src/hooks/useWorkflowDocuments.tsx +76 -78
- package/src/hooks/useWorkflowMetadata.tsx +31 -26
- package/src/index.ts +60 -57
- package/src/schema/workflow/workflow.metadata.ts +68 -0
- package/src/tools/index.ts +15 -0
- package/src/types/index.ts +27 -6
- package/src/actions/DemoteAction.tsx +0 -62
- package/src/actions/PromoteAction.tsx +0 -62
- package/src/actions/RequestReviewAction.js +0 -61
- package/src/actions/index.js +0 -21
- package/src/badges/index.tsx +0 -31
- package/src/components/Mutate.tsx +0 -54
- package/src/components/StateTimeline.tsx +0 -98
- package/src/components/UserSelectInput.tsx +0 -43
- package/src/schema/workflow/metadata.ts +0 -38
|
@@ -1,69 +1,56 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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 {
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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<
|
|
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
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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 (
|
|
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
|
-
//
|
|
113
|
-
const
|
|
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 (
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
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
|
|
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
|
-
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
{
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
{
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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,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
|
+
}
|