sanity-plugin-workflow 1.0.0-beta.1 → 1.0.0-beta.4
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 +73 -12
- package/lib/{src/index.d.ts → index.d.ts} +3 -3
- package/lib/index.esm.js +1800 -1
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +1813 -1
- package/lib/index.js.map +1 -1
- package/package.json +51 -40
- package/src/actions/AssignWorkflow.tsx +48 -0
- package/src/actions/BeginWorkflow.tsx +68 -0
- package/src/actions/CompleteWorkflow.tsx +41 -0
- package/src/actions/RequestReviewAction.js +1 -7
- package/src/actions/UpdateWorkflow.tsx +142 -0
- package/src/badges/AssigneesBadge.tsx +52 -0
- package/src/badges/{index.tsx → StateBadge.tsx} +4 -8
- package/src/components/DocumentCard/AvatarGroup.tsx +12 -8
- package/src/components/DocumentCard/CompleteButton.tsx +53 -0
- package/src/components/DocumentCard/EditButton.tsx +3 -2
- package/src/components/DocumentCard/Field.tsx +38 -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 +156 -50
- package/src/components/DocumentList.tsx +122 -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 +73 -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/Validators.tsx +229 -0
- package/src/components/WorkflowTool.tsx +302 -163
- package/src/constants/index.ts +31 -0
- package/src/helpers/arraysContainMatchingString.ts +6 -0
- package/src/helpers/filterItemsAndSort.ts +41 -0
- package/src/helpers/initialRank.ts +13 -0
- package/src/hooks/useWorkflowDocuments.tsx +62 -70
- package/src/hooks/useWorkflowMetadata.tsx +0 -1
- package/src/index.ts +38 -58
- 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/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,68 +1,43 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import {
|
|
3
|
-
Flex,
|
|
4
|
-
Card,
|
|
5
|
-
Box,
|
|
6
|
-
Grid,
|
|
7
|
-
Spinner,
|
|
8
|
-
Label,
|
|
9
|
-
useToast,
|
|
10
|
-
Container,
|
|
11
|
-
useTheme,
|
|
12
|
-
Button,
|
|
13
|
-
} from '@sanity/ui'
|
|
14
|
-
import {Feedback, useProjectUsers} from 'sanity-plugin-utils'
|
|
15
|
-
import {Tool, useClient} from 'sanity'
|
|
16
1
|
import {
|
|
17
2
|
DragDropContext,
|
|
3
|
+
DragStart,
|
|
18
4
|
Droppable,
|
|
19
|
-
Draggable,
|
|
20
5
|
DropResult,
|
|
21
|
-
} from '
|
|
6
|
+
} from '@hello-pangea/dnd'
|
|
7
|
+
import {Box, Card, Container, Flex, Grid, Spinner, useTheme} from '@sanity/ui'
|
|
8
|
+
import {LexoRank} from 'lexorank'
|
|
9
|
+
import React from 'react'
|
|
10
|
+
import {Tool, useCurrentUser} from 'sanity'
|
|
11
|
+
import {Feedback, useProjectUsers} from 'sanity-plugin-utils'
|
|
22
12
|
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import
|
|
13
|
+
import {API_VERSION} from '../constants'
|
|
14
|
+
import {arraysContainMatchingString} from '../helpers/arraysContainMatchingString'
|
|
15
|
+
import {filterItemsAndSort} from '../helpers/filterItemsAndSort'
|
|
26
16
|
import {useWorkflowDocuments} from '../hooks/useWorkflowDocuments'
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
type WorkflowToolOptions = {
|
|
36
|
-
schemaTypes: string[]
|
|
37
|
-
states: State[]
|
|
38
|
-
}
|
|
17
|
+
import {State, WorkflowConfig} from '../types'
|
|
18
|
+
import {DocumentCard} from './DocumentCard'
|
|
19
|
+
import DocumentList from './DocumentList'
|
|
20
|
+
import Filters from './Filters'
|
|
21
|
+
import StateTitle from './StateTitle'
|
|
22
|
+
import Validators from './Validators'
|
|
39
23
|
|
|
40
24
|
type WorkflowToolProps = {
|
|
41
|
-
tool: Tool<
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type MutateProps = {
|
|
45
|
-
_id: string
|
|
46
|
-
_type: string
|
|
47
|
-
state: State
|
|
48
|
-
documentId: string
|
|
25
|
+
tool: Tool<WorkflowConfig>
|
|
49
26
|
}
|
|
50
27
|
|
|
51
28
|
export default function WorkflowTool(props: WorkflowToolProps) {
|
|
52
29
|
const {schemaTypes = [], states = []} = props?.tool?.options ?? {}
|
|
53
30
|
|
|
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
31
|
const isDarkMode = useTheme().sanity.color.dark
|
|
63
32
|
const defaultCardTone = isDarkMode ? 'default' : 'transparent'
|
|
64
33
|
|
|
65
|
-
const userList = useProjectUsers()
|
|
34
|
+
const userList = useProjectUsers({apiVersion: API_VERSION})
|
|
35
|
+
|
|
36
|
+
const user = useCurrentUser()
|
|
37
|
+
const userRoleNames = user?.roles?.length
|
|
38
|
+
? user?.roles.map((r) => r.name)
|
|
39
|
+
: []
|
|
40
|
+
|
|
66
41
|
const {workflowData, operations} = useWorkflowDocuments(schemaTypes)
|
|
67
42
|
|
|
68
43
|
// Data to display in cards
|
|
@@ -71,54 +46,182 @@ export default function WorkflowTool(props: WorkflowToolProps) {
|
|
|
71
46
|
// Operations to perform on cards
|
|
72
47
|
const {move} = operations
|
|
73
48
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
49
|
+
const [undroppableStates, setUndroppableStates] = React.useState<string[]>([])
|
|
50
|
+
const [draggingFrom, setDraggingFrom] = React.useState(``)
|
|
51
|
+
|
|
52
|
+
// When drag starts, check for any States we should not allow dropping on
|
|
53
|
+
// Because of either:
|
|
54
|
+
// 1. The "destination" State requires user assignment and the user is not assigned to the dragged document
|
|
55
|
+
// 2. The "source" State State has a list of transitions and the "destination" State is not in that list
|
|
56
|
+
const handleDragStart = React.useCallback(
|
|
57
|
+
(start: DragStart) => {
|
|
58
|
+
const {draggableId, source} = start
|
|
59
|
+
const {droppableId: currentStateId} = source
|
|
60
|
+
setDraggingFrom(currentStateId)
|
|
61
|
+
|
|
62
|
+
const document = data.find(
|
|
63
|
+
(item) => item._metadata?.documentId === draggableId
|
|
64
|
+
)
|
|
65
|
+
const state = states.find((s) => s.id === currentStateId)
|
|
66
|
+
|
|
67
|
+
// This shouldn't happen but TypeScript
|
|
68
|
+
if (!document || !state) return
|
|
69
|
+
|
|
70
|
+
const undroppableStateIds = []
|
|
71
|
+
const statesThatRequireAssignmentIds = states
|
|
72
|
+
.filter((s) => s.requireAssignment)
|
|
73
|
+
.map((s) => s.id)
|
|
74
|
+
|
|
75
|
+
if (statesThatRequireAssignmentIds.length) {
|
|
76
|
+
const documentAssignees = document._metadata?.assignees ?? []
|
|
77
|
+
const userIsAssignedToDocument = user?.id
|
|
78
|
+
? documentAssignees.includes(user.id)
|
|
79
|
+
: false
|
|
80
|
+
|
|
81
|
+
if (!userIsAssignedToDocument) {
|
|
82
|
+
undroppableStateIds.push(...statesThatRequireAssignmentIds)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const statesThatCannotBeTransitionedToIds =
|
|
87
|
+
state.transitions && state.transitions.length
|
|
88
|
+
? states
|
|
89
|
+
.filter((s) => !state.transitions?.includes(s.id))
|
|
90
|
+
.map((s) => s.id)
|
|
91
|
+
: []
|
|
92
|
+
|
|
93
|
+
if (statesThatCannotBeTransitionedToIds.length) {
|
|
94
|
+
undroppableStateIds.push(...statesThatCannotBeTransitionedToIds)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Remove currentStateId from undroppableStates
|
|
98
|
+
const undroppableExceptSelf = undroppableStateIds.filter(
|
|
99
|
+
(id) => id !== currentStateId
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if (undroppableExceptSelf.length) {
|
|
103
|
+
setUndroppableStates(undroppableExceptSelf)
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
[data, states, user]
|
|
107
|
+
)
|
|
100
108
|
|
|
101
109
|
const handleDragEnd = React.useCallback(
|
|
102
110
|
(result: DropResult) => {
|
|
111
|
+
// Reset undroppable states
|
|
112
|
+
setUndroppableStates([])
|
|
113
|
+
setDraggingFrom(``)
|
|
114
|
+
|
|
103
115
|
const {draggableId, source, destination} = result
|
|
104
|
-
console.log(
|
|
105
|
-
`sending ${draggableId} from ${source.droppableId} to ${destination?.droppableId}`
|
|
106
|
-
)
|
|
107
116
|
|
|
108
|
-
if (
|
|
117
|
+
if (
|
|
118
|
+
// No destination?
|
|
119
|
+
!destination ||
|
|
120
|
+
// No change in position?
|
|
121
|
+
(destination.droppableId === source.droppableId &&
|
|
122
|
+
destination.index === source.index)
|
|
123
|
+
) {
|
|
109
124
|
return
|
|
110
125
|
}
|
|
111
126
|
|
|
112
|
-
//
|
|
113
|
-
const
|
|
127
|
+
// Find all items in current state
|
|
128
|
+
const destinationStateItems = [
|
|
129
|
+
...filterItemsAndSort(data, destination.droppableId, [], null),
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
let newOrder
|
|
114
133
|
|
|
115
|
-
if (
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
134
|
+
if (!destinationStateItems.length) {
|
|
135
|
+
// Only item in state
|
|
136
|
+
// New minimum rank
|
|
137
|
+
newOrder = LexoRank.min().toString()
|
|
138
|
+
} else if (destination.index === 0) {
|
|
139
|
+
// Now first item in order
|
|
140
|
+
const firstItemOrderRank = [...destinationStateItems].shift()?._metadata
|
|
141
|
+
?.orderRank
|
|
142
|
+
newOrder =
|
|
143
|
+
firstItemOrderRank && typeof firstItemOrderRank === 'string'
|
|
144
|
+
? LexoRank.parse(firstItemOrderRank).genPrev().toString()
|
|
145
|
+
: LexoRank.min().toString()
|
|
146
|
+
} else if (destination.index + 1 === destinationStateItems.length) {
|
|
147
|
+
// Now last item in order
|
|
148
|
+
const lastItemOrderRank = [...destinationStateItems].pop()?._metadata
|
|
149
|
+
?.orderRank
|
|
150
|
+
newOrder =
|
|
151
|
+
lastItemOrderRank && typeof lastItemOrderRank === 'string'
|
|
152
|
+
? LexoRank.parse(lastItemOrderRank).genNext().toString()
|
|
153
|
+
: LexoRank.min().toString()
|
|
154
|
+
} else {
|
|
155
|
+
// Must be between two items
|
|
156
|
+
const itemBefore = destinationStateItems[destination.index - 1]
|
|
157
|
+
const itemBeforeRank = itemBefore?._metadata?.orderRank
|
|
158
|
+
const itemBeforeRankParsed = itemBefore._metadata.orderRank
|
|
159
|
+
? LexoRank.parse(itemBeforeRank)
|
|
160
|
+
: LexoRank.min()
|
|
161
|
+
const itemAfter = destinationStateItems[destination.index]
|
|
162
|
+
const itemAfterRank = itemAfter?._metadata?.orderRank
|
|
163
|
+
const itemAfterRankParsed = itemAfter._metadata.orderRank
|
|
164
|
+
? LexoRank.parse(itemAfterRank)
|
|
165
|
+
: LexoRank.max()
|
|
166
|
+
|
|
167
|
+
newOrder = itemBeforeRankParsed.between(itemAfterRankParsed).toString()
|
|
119
168
|
}
|
|
169
|
+
|
|
170
|
+
move(draggableId, destination, states, newOrder)
|
|
120
171
|
},
|
|
121
|
-
[move, states]
|
|
172
|
+
[data, move, states]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
// Used for the user filter UI
|
|
176
|
+
const uniqueAssignedUsers = React.useMemo(() => {
|
|
177
|
+
const uniqueUserIds = data.reduce((acc, item) => {
|
|
178
|
+
const {assignees = []} = item._metadata ?? {}
|
|
179
|
+
const newAssignees = assignees?.length
|
|
180
|
+
? assignees.filter((a) => !acc.includes(a))
|
|
181
|
+
: []
|
|
182
|
+
return newAssignees.length ? [...acc, ...newAssignees] : acc
|
|
183
|
+
}, [] as string[])
|
|
184
|
+
|
|
185
|
+
return userList.filter((u) => uniqueUserIds.includes(u.id))
|
|
186
|
+
}, [data, userList])
|
|
187
|
+
|
|
188
|
+
// Selected user IDs filter the visible workflow documents
|
|
189
|
+
const [selectedUserIds, setSelectedUserIds] = React.useState<string[]>(
|
|
190
|
+
uniqueAssignedUsers.map((u) => u.id)
|
|
191
|
+
)
|
|
192
|
+
const toggleSelectedUser = React.useCallback((userId: string) => {
|
|
193
|
+
setSelectedUserIds((prev) =>
|
|
194
|
+
prev.includes(userId)
|
|
195
|
+
? prev.filter((u) => u !== userId)
|
|
196
|
+
: [...prev, userId]
|
|
197
|
+
)
|
|
198
|
+
}, [])
|
|
199
|
+
const resetSelectedUsers = React.useCallback(() => {
|
|
200
|
+
setSelectedUserIds([])
|
|
201
|
+
}, [])
|
|
202
|
+
|
|
203
|
+
// Selected schema types filter the visible workflow documents
|
|
204
|
+
const [selectedSchemaTypes, setSelectedSchemaTypes] =
|
|
205
|
+
React.useState<string[]>(schemaTypes)
|
|
206
|
+
const toggleSelectedSchemaType = React.useCallback((schemaType: string) => {
|
|
207
|
+
setSelectedSchemaTypes((prev) =>
|
|
208
|
+
prev.includes(schemaType)
|
|
209
|
+
? prev.filter((u) => u !== schemaType)
|
|
210
|
+
: [...prev, schemaType]
|
|
211
|
+
)
|
|
212
|
+
}, [])
|
|
213
|
+
|
|
214
|
+
// Document IDs that have validation errors
|
|
215
|
+
const [invalidDocumentIds, setInvalidDocumentIds] = React.useState<string[]>(
|
|
216
|
+
[]
|
|
217
|
+
)
|
|
218
|
+
const toggleInvalidDocumentId = React.useCallback(
|
|
219
|
+
(docId: string, action: 'ADD' | 'REMOVE') => {
|
|
220
|
+
setInvalidDocumentIds((prev) =>
|
|
221
|
+
action === 'ADD' ? [...prev, docId] : prev.filter((id) => id !== docId)
|
|
222
|
+
)
|
|
223
|
+
},
|
|
224
|
+
[]
|
|
122
225
|
)
|
|
123
226
|
|
|
124
227
|
if (!states?.length) {
|
|
@@ -133,96 +236,132 @@ export default function WorkflowTool(props: WorkflowToolProps) {
|
|
|
133
236
|
)
|
|
134
237
|
}
|
|
135
238
|
|
|
136
|
-
if (error) {
|
|
239
|
+
if (error && !data.length) {
|
|
137
240
|
return (
|
|
138
241
|
<Container width={1} padding={5}>
|
|
139
|
-
<Feedback
|
|
242
|
+
<Feedback
|
|
243
|
+
tone="critical"
|
|
244
|
+
title="Error querying for Workflow documents"
|
|
245
|
+
/>
|
|
140
246
|
</Container>
|
|
141
247
|
)
|
|
142
248
|
}
|
|
143
249
|
|
|
144
250
|
return (
|
|
145
|
-
|
|
146
|
-
{
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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}>
|
|
251
|
+
<Flex direction="column" height="fill" overflow="hidden">
|
|
252
|
+
<Validators data={data} userList={userList} states={states} />
|
|
253
|
+
<Filters
|
|
254
|
+
uniqueAssignedUsers={uniqueAssignedUsers}
|
|
255
|
+
selectedUserIds={selectedUserIds}
|
|
256
|
+
toggleSelectedUser={toggleSelectedUser}
|
|
257
|
+
resetSelectedUsers={resetSelectedUsers}
|
|
258
|
+
schemaTypes={schemaTypes}
|
|
259
|
+
selectedSchemaTypes={selectedSchemaTypes}
|
|
260
|
+
toggleSelectedSchemaType={toggleSelectedSchemaType}
|
|
261
|
+
/>
|
|
262
|
+
<DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
|
|
175
263
|
<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
|
-
|
|
264
|
+
{states.map((state: State, stateIndex: number) => {
|
|
265
|
+
const userRoleCanDrop = state?.roles?.length
|
|
266
|
+
? arraysContainMatchingString(state.roles, userRoleNames)
|
|
267
|
+
: true
|
|
268
|
+
const isDropDisabled =
|
|
269
|
+
!userRoleCanDrop || undroppableStates.includes(state.id)
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<Card
|
|
273
|
+
key={state.id}
|
|
274
|
+
borderLeft={stateIndex > 0}
|
|
275
|
+
tone={defaultCardTone}
|
|
276
|
+
>
|
|
277
|
+
<Flex direction="column" height="fill">
|
|
278
|
+
<StateTitle
|
|
279
|
+
state={state}
|
|
280
|
+
requireAssignment={state.requireAssignment ?? false}
|
|
281
|
+
userRoleCanDrop={userRoleCanDrop}
|
|
282
|
+
// operation={state.operation}
|
|
283
|
+
isDropDisabled={isDropDisabled}
|
|
284
|
+
draggingFrom={draggingFrom}
|
|
285
|
+
/>
|
|
286
|
+
<Box flex={1}>
|
|
287
|
+
<Droppable
|
|
288
|
+
droppableId={state.id}
|
|
289
|
+
isDropDisabled={isDropDisabled}
|
|
290
|
+
// props required for virtualization
|
|
291
|
+
mode="virtual"
|
|
292
|
+
renderClone={(provided, snapshot, rubric) => {
|
|
293
|
+
const item = data.find(
|
|
294
|
+
(doc) =>
|
|
295
|
+
doc?._metadata?.documentId === rubric.draggableId
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<div
|
|
300
|
+
{...provided.draggableProps}
|
|
301
|
+
{...provided.dragHandleProps}
|
|
302
|
+
ref={provided.innerRef}
|
|
202
303
|
>
|
|
203
|
-
{
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
{
|
|
207
|
-
{
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
304
|
+
{item ? (
|
|
305
|
+
<DocumentCard
|
|
306
|
+
isDragDisabled={false}
|
|
307
|
+
userRoleCanDrop={userRoleCanDrop}
|
|
308
|
+
isDragging={snapshot.isDragging}
|
|
309
|
+
item={item}
|
|
310
|
+
states={states}
|
|
311
|
+
toggleInvalidDocumentId={
|
|
312
|
+
toggleInvalidDocumentId
|
|
313
|
+
}
|
|
314
|
+
userList={userList}
|
|
315
|
+
/>
|
|
316
|
+
) : (
|
|
317
|
+
<Feedback title="Item not found" tone="caution" />
|
|
215
318
|
)}
|
|
216
|
-
</
|
|
319
|
+
</div>
|
|
217
320
|
)
|
|
321
|
+
}}
|
|
322
|
+
>
|
|
323
|
+
{(provided, snapshot) => (
|
|
324
|
+
<Card
|
|
325
|
+
ref={provided.innerRef}
|
|
326
|
+
tone={
|
|
327
|
+
snapshot.isDraggingOver
|
|
328
|
+
? `primary`
|
|
329
|
+
: defaultCardTone
|
|
330
|
+
}
|
|
331
|
+
height="fill"
|
|
332
|
+
paddingTop={1}
|
|
333
|
+
>
|
|
334
|
+
{loading ? (
|
|
335
|
+
<Flex padding={5} align="center" justify="center">
|
|
336
|
+
<Spinner muted />
|
|
337
|
+
</Flex>
|
|
338
|
+
) : null}
|
|
339
|
+
|
|
340
|
+
<DocumentList
|
|
341
|
+
data={data}
|
|
342
|
+
invalidDocumentIds={invalidDocumentIds}
|
|
343
|
+
selectedSchemaTypes={selectedSchemaTypes}
|
|
344
|
+
selectedUserIds={selectedUserIds}
|
|
345
|
+
state={state}
|
|
346
|
+
states={states}
|
|
347
|
+
toggleInvalidDocumentId={toggleInvalidDocumentId}
|
|
348
|
+
user={user}
|
|
349
|
+
userList={userList}
|
|
350
|
+
userRoleCanDrop={userRoleCanDrop}
|
|
351
|
+
/>
|
|
352
|
+
|
|
353
|
+
{/* Not required for virtualized lists */}
|
|
354
|
+
{/* {provided.placeholder} */}
|
|
355
|
+
</Card>
|
|
218
356
|
)}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
357
|
+
</Droppable>
|
|
358
|
+
</Box>
|
|
359
|
+
</Flex>
|
|
360
|
+
</Card>
|
|
361
|
+
)
|
|
362
|
+
})}
|
|
224
363
|
</Grid>
|
|
225
364
|
</DragDropContext>
|
|
226
|
-
|
|
365
|
+
</Flex>
|
|
227
366
|
)
|
|
228
367
|
}
|
|
@@ -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: 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
|
+
requireAssignment: true,
|
|
28
|
+
transitions: ['changesRequested'],
|
|
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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {LexoRank} from 'lexorank'
|
|
2
|
+
|
|
3
|
+
// Use in initial value field by passing in the rank value of the last document
|
|
4
|
+
// If not value passed, generate a sensibly low rank
|
|
5
|
+
export default function initialRank(lastRankValue = ``): string {
|
|
6
|
+
const lastRank =
|
|
7
|
+
lastRankValue && typeof lastRankValue === 'string'
|
|
8
|
+
? LexoRank.parse(lastRankValue)
|
|
9
|
+
: LexoRank.min()
|
|
10
|
+
const nextRank = lastRank.genNext().genNext()
|
|
11
|
+
|
|
12
|
+
return (nextRank as any).value
|
|
13
|
+
}
|