sanity-plugin-workflow 1.0.0-beta.6 → 1.0.0-beta.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-workflow",
3
- "version": "1.0.0-beta.6",
3
+ "version": "1.0.0-beta.8",
4
4
  "description": "A demonstration of a custom content publishing workflow using Sanity.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -87,7 +87,7 @@
87
87
  "rimraf": "^4.1.2",
88
88
  "sanity": "^3.3.1",
89
89
  "semantic-release": "^20.1.0",
90
- "typescript": "^4.9.5"
90
+ "typescript": "^5.0.0"
91
91
  },
92
92
  "peerDependencies": {
93
93
  "@sanity/ui": "^1.2.2",
@@ -1,6 +1,6 @@
1
- import React from 'react'
2
- import {Button, useToast} from '@sanity/ui'
3
1
  import {CheckmarkIcon} from '@sanity/icons'
2
+ import {Box, Button, Text, Tooltip, useToast} from '@sanity/ui'
3
+ import React from 'react'
4
4
  import {useClient} from 'sanity'
5
5
 
6
6
  import {API_VERSION} from '../../constants'
@@ -15,39 +15,54 @@ export default function CompleteButton(props: CompleteButtonProps) {
15
15
  const client = useClient({apiVersion: API_VERSION})
16
16
  const toast = useToast()
17
17
 
18
- const handleComplete = React.useCallback(
19
- (id: string) => {
20
- client
21
- .delete(`workflow-metadata.${id}`)
22
- .then(() => {
23
- toast.push({
24
- status: 'success',
25
- title: 'Workflow completed',
26
- description: id,
18
+ const handleComplete: React.MouseEventHandler<HTMLButtonElement> =
19
+ React.useCallback(
20
+ (event) => {
21
+ const id = event.currentTarget.value
22
+
23
+ if (!id) {
24
+ return
25
+ }
26
+
27
+ client
28
+ .delete(`workflow-metadata.${id}`)
29
+ .then(() => {
30
+ toast.push({
31
+ status: 'success',
32
+ title: 'Workflow completed',
33
+ })
27
34
  })
28
- })
29
- .catch(() => {
30
- toast.push({
31
- status: 'error',
32
- title: 'Could not complete Workflow',
33
- description: id,
35
+ .catch(() => {
36
+ toast.push({
37
+ status: 'error',
38
+ title: 'Could not complete Workflow',
39
+ })
34
40
  })
35
- })
36
- },
37
- [client, toast]
38
- )
41
+ },
42
+ [client, toast]
43
+ )
39
44
 
40
45
  return (
41
- <Button
42
- onClick={() => handleComplete(documentId)}
43
- text="Complete"
44
- icon={CheckmarkIcon}
45
- tone="positive"
46
- mode="ghost"
47
- fontSize={1}
48
- padding={2}
49
- tabIndex={-1}
50
- disabled={disabled}
51
- />
46
+ <Tooltip
47
+ portal
48
+ content={
49
+ <Box padding={2}>
50
+ <Text size={1}>Remove this document from Workflow</Text>
51
+ </Box>
52
+ }
53
+ >
54
+ <Button
55
+ value={documentId}
56
+ onClick={handleComplete}
57
+ text="Complete"
58
+ icon={CheckmarkIcon}
59
+ tone="positive"
60
+ mode="ghost"
61
+ fontSize={1}
62
+ padding={2}
63
+ tabIndex={-1}
64
+ disabled={disabled}
65
+ />
66
+ </Tooltip>
52
67
  )
53
68
  }
@@ -20,6 +20,7 @@ import {ValidationStatus} from './ValidationStatus'
20
20
 
21
21
  type DocumentCardProps = {
22
22
  isDragDisabled: boolean
23
+ isPatching: boolean
23
24
  userRoleCanDrop: boolean
24
25
  isDragging: boolean
25
26
  item: SanityDocumentWithMetadata
@@ -34,6 +35,7 @@ type DocumentCardProps = {
34
35
  export function DocumentCard(props: DocumentCardProps) {
35
36
  const {
36
37
  isDragDisabled,
38
+ isPatching,
37
39
  userRoleCanDrop,
38
40
  isDragging,
39
41
  item,
@@ -68,6 +70,7 @@ export function DocumentCard(props: DocumentCardProps) {
68
70
 
69
71
  if (!userRoleCanDrop) return isDarkMode ? `default` : `transparent`
70
72
  if (!documentId) return tone
73
+ if (isPatching) tone = isDarkMode ? `default` : `transparent`
71
74
  if (isDragging) tone = `positive`
72
75
 
73
76
  if (state?.requireValidation && !isValidating && validation.length > 0) {
@@ -82,6 +85,7 @@ export function DocumentCard(props: DocumentCardProps) {
82
85
  }, [
83
86
  defaultCardTone,
84
87
  userRoleCanDrop,
88
+ isPatching,
85
89
  isDarkMode,
86
90
  documentId,
87
91
  isDragging,
@@ -137,13 +141,18 @@ export function DocumentCard(props: DocumentCardProps) {
137
141
  <Flex align="center" justify="space-between" gap={1}>
138
142
  <Box flex={1}>
139
143
  <Preview
140
- layout="default"
144
+ // Like as in desk lists, except it has an intermittent loading state
145
+ // layout="default"
146
+ // Like in the PTE, with no loading state
147
+ layout="block"
141
148
  value={item}
142
149
  schemaType={schema.get(item._type) as SchemaType}
143
150
  />
144
151
  </Box>
145
152
  <Box style={{flexShrink: 0}}>
146
- {hasError || isDragDisabled ? null : <DragHandleIcon />}
153
+ {hasError || isDragDisabled || isPatching ? null : (
154
+ <DragHandleIcon />
155
+ )}
147
156
  </Box>
148
157
  </Flex>
149
158
  </Card>
@@ -170,13 +179,21 @@ export function DocumentCard(props: DocumentCardProps) {
170
179
  type={item._type}
171
180
  disabled={!userRoleCanDrop}
172
181
  />
173
- {isLastState ? (
182
+ {isLastState && states.length <= 3 ? (
174
183
  <CompleteButton
175
184
  documentId={documentId}
176
185
  disabled={!userRoleCanDrop}
177
186
  />
178
187
  ) : null}
179
188
  </Flex>
189
+ {isLastState && states.length > 3 ? (
190
+ <Stack paddingTop={2}>
191
+ <CompleteButton
192
+ documentId={documentId}
193
+ disabled={!userRoleCanDrop}
194
+ />
195
+ </Stack>
196
+ ) : null}
180
197
  </Card>
181
198
  </Stack>
182
199
  </Card>
@@ -11,6 +11,7 @@ import {DocumentCard} from './DocumentCard'
11
11
  type DocumentListProps = {
12
12
  data: SanityDocumentWithMetadata[]
13
13
  invalidDocumentIds: string[]
14
+ patchingIds: string[]
14
15
  selectedSchemaTypes: string[]
15
16
  selectedUserIds: string[]
16
17
  state: State
@@ -28,6 +29,7 @@ export default function DocumentList(props: DocumentListProps) {
28
29
  const {
29
30
  data = [],
30
31
  invalidDocumentIds,
32
+ patchingIds,
31
33
  selectedSchemaTypes,
32
34
  selectedUserIds,
33
35
  state,
@@ -70,7 +72,6 @@ export default function DocumentList(props: DocumentListProps) {
70
72
  scrollBehavior: 'auto',
71
73
  }}
72
74
  >
73
- {/* {dataFiltered.map((item, itemIndex) => { */}
74
75
  {rowVirtualizer.getVirtualItems().map((virtualItem) => {
75
76
  const item = dataFiltered[virtualItem.index]
76
77
 
@@ -83,6 +84,7 @@ export default function DocumentList(props: DocumentListProps) {
83
84
  const isInvalid = invalidDocumentIds.includes(documentId)
84
85
  const meInAssignees = user?.id ? assignees?.includes(user.id) : false
85
86
  const isDragDisabled =
87
+ patchingIds.includes(documentId) ||
86
88
  !userRoleCanDrop ||
87
89
  isInvalid ||
88
90
  !(state.requireAssignment
@@ -92,7 +94,8 @@ export default function DocumentList(props: DocumentListProps) {
92
94
  return (
93
95
  <Draggable
94
96
  // The metadata's documentId is always the published one to avoid rerendering
95
- key={documentId}
97
+ // key={documentId}
98
+ key={virtualItem.key}
96
99
  draggableId={documentId}
97
100
  index={virtualItem.index}
98
101
  isDragDisabled={isDragDisabled}
@@ -106,6 +109,7 @@ export default function DocumentList(props: DocumentListProps) {
106
109
  <DocumentCard
107
110
  userRoleCanDrop={userRoleCanDrop}
108
111
  isDragDisabled={isDragDisabled}
112
+ isPatching={patchingIds.includes(documentId)}
109
113
  isDragging={draggableSnapshot.isDragging}
110
114
  item={item}
111
115
  toggleInvalidDocumentId={toggleInvalidDocumentId}
@@ -139,7 +139,7 @@ export default function Filters(props: FiltersProps) {
139
139
  )}
140
140
  </Flex>
141
141
 
142
- {schemaTypes.length > 0 ? (
142
+ {schemaTypes.length > 1 ? (
143
143
  <Flex align="center" gap={1}>
144
144
  {schemaTypes.map((typeName) => {
145
145
  const schemaType = schema.get(typeName)
@@ -5,6 +5,7 @@ import {useClient} from 'sanity'
5
5
  import {UserExtended} from 'sanity-plugin-utils'
6
6
 
7
7
  import {API_VERSION} from '../constants'
8
+ import {generateMultipleOrderRanks} from '../helpers/generateMultipleOrderRanks'
8
9
  import {SanityDocumentWithMetadata, State} from '../types'
9
10
  import FloatingCard from './FloatingCard'
10
11
 
@@ -32,16 +33,17 @@ export default function Verify(props: VerifyProps) {
32
33
  }, [] as string[])
33
34
  : []
34
35
 
35
- const documentsWithInvalidUserIds = data?.length
36
- ? data.reduce((acc, cur) => {
37
- const {documentId, assignees} = cur._metadata ?? {}
38
- const allAssigneesExist = assignees?.length
39
- ? assignees?.every((a) => userList.find((u) => u.id === a))
40
- : true
36
+ const documentsWithInvalidUserIds =
37
+ data?.length && userList?.length
38
+ ? data.reduce((acc, cur) => {
39
+ const {documentId, assignees} = cur._metadata ?? {}
40
+ const allAssigneesExist = assignees?.length
41
+ ? assignees?.every((a) => userList.find((u) => u.id === a))
42
+ : true
41
43
 
42
- return !allAssigneesExist && documentId ? [...acc, documentId] : acc
43
- }, [] as string[])
44
- : []
44
+ return !allAssigneesExist && documentId ? [...acc, documentId] : acc
45
+ }, [] as string[])
46
+ : []
45
47
 
46
48
  const documentsWithoutOrderIds = data?.length
47
49
  ? data.reduce((acc, cur) => {
@@ -130,20 +132,51 @@ export default function Verify(props: VerifyProps) {
130
132
  status: 'info',
131
133
  })
132
134
 
133
- // Get first order value
134
- const firstOrder = data[0]?._metadata?.orderRank
135
- let newLexo =
136
- firstOrder && data.length !== ids.length
137
- ? LexoRank.parse(firstOrder)
138
- : LexoRank.min()
135
+ // Get first and second order values, if they exist
136
+ const [firstOrder, secondOrder] = [...data]
137
+ .slice(0, 2)
138
+ .map((d) => d._metadata?.orderRank)
139
+ const minLexo = firstOrder ? LexoRank.parse(firstOrder) : undefined
140
+ const maxLexo = secondOrder ? LexoRank.parse(secondOrder) : undefined
141
+ const ranks = generateMultipleOrderRanks(ids.length, minLexo, maxLexo)
139
142
 
140
143
  const tx = client.transaction()
141
144
 
145
+ // Create a new in-between value for each document
142
146
  for (let index = 0; index < ids.length; index += 1) {
143
- newLexo = newLexo.genNext().genNext()
147
+ tx.patch(`workflow-metadata.${ids[index]}`, {
148
+ set: {orderRank: ranks[index].toString()},
149
+ })
150
+ }
151
+
152
+ await tx.commit()
153
+
154
+ toast.push({
155
+ title: `Added order to ${
156
+ ids.length === 1 ? `1 Document` : `${ids.length} Documents`
157
+ }`,
158
+ status: 'success',
159
+ })
160
+ },
161
+ [data, client, toast]
162
+ )
163
+
164
+ // Reset order value on all metadata documents
165
+ const resetOrderOfAllDocuments = React.useCallback(
166
+ async (ids: string[]) => {
167
+ toast.push({
168
+ title: 'Adding ordering...',
169
+ status: 'info',
170
+ })
171
+
172
+ const ranks = generateMultipleOrderRanks(ids.length)
144
173
 
174
+ const tx = client.transaction()
175
+
176
+ // Create a new in-between value for each document
177
+ for (let index = 0; index < ids.length; index += 1) {
145
178
  tx.patch(`workflow-metadata.${ids[index]}`, {
146
- set: {orderRank: newLexo.toString()},
179
+ set: {orderRank: ranks[index].toString()},
147
180
  })
148
181
  }
149
182
 
@@ -190,6 +223,7 @@ export default function Verify(props: VerifyProps) {
190
223
  {documentsWithoutValidMetadataIds.length > 0 ? (
191
224
  <Button
192
225
  tone="caution"
226
+ mode="ghost"
193
227
  onClick={() => correctDocuments(documentsWithoutValidMetadataIds)}
194
228
  text={
195
229
  documentsWithoutValidMetadataIds.length === 1
@@ -201,6 +235,7 @@ export default function Verify(props: VerifyProps) {
201
235
  {documentsWithInvalidUserIds.length > 0 ? (
202
236
  <Button
203
237
  tone="caution"
238
+ mode="ghost"
204
239
  onClick={() => removeUsersFromDocuments(documentsWithInvalidUserIds)}
205
240
  text={
206
241
  documentsWithInvalidUserIds.length === 1
@@ -212,6 +247,7 @@ export default function Verify(props: VerifyProps) {
212
247
  {documentsWithoutOrderIds.length > 0 ? (
213
248
  <Button
214
249
  tone="caution"
250
+ mode="ghost"
215
251
  onClick={() => addOrderToDocuments(documentsWithoutOrderIds)}
216
252
  text={
217
253
  documentsWithoutOrderIds.length === 1
@@ -221,36 +257,41 @@ export default function Verify(props: VerifyProps) {
221
257
  />
222
258
  ) : null}
223
259
  {documentsWithDuplicatedOrderIds.length > 0 ? (
224
- <Button
225
- tone="caution"
226
- onClick={() => addOrderToDocuments(documentsWithDuplicatedOrderIds)}
227
- text={
228
- documentsWithDuplicatedOrderIds.length === 1
229
- ? `Set Unique Order for 1 Document`
230
- : `Set Unique Order for ${documentsWithDuplicatedOrderIds.length} Documents`
231
- }
232
- />
260
+ <>
261
+ <Button
262
+ tone="caution"
263
+ mode="ghost"
264
+ onClick={() => addOrderToDocuments(documentsWithDuplicatedOrderIds)}
265
+ text={
266
+ documentsWithDuplicatedOrderIds.length === 1
267
+ ? `Set Unique Order for 1 Document`
268
+ : `Set Unique Order for ${documentsWithDuplicatedOrderIds.length} Documents`
269
+ }
270
+ />
271
+ <Button
272
+ tone="caution"
273
+ mode="ghost"
274
+ onClick={() =>
275
+ resetOrderOfAllDocuments(
276
+ data.map((doc) => String(doc._metadata?.documentId))
277
+ )
278
+ }
279
+ text={
280
+ data.length === 1
281
+ ? `Reset Order for 1 Document`
282
+ : `Reset Order for all ${data.length} Documents`
283
+ }
284
+ />
285
+ </>
233
286
  ) : null}
234
287
  {orphanedMetadataDocumentIds.length > 0 ? (
235
288
  <Button
236
289
  text="Cleanup orphaned metadata"
237
290
  onClick={handleOrphans}
238
291
  tone="caution"
292
+ mode="ghost"
239
293
  />
240
294
  ) : null}
241
- {/* <Button
242
- tone="caution"
243
- onClick={() =>
244
- addOrderToDocuments(
245
- data.map((doc) => String(doc._metadata?.documentId))
246
- )
247
- }
248
- text={
249
- data.length === 1
250
- ? `Reset Order for 1 Document`
251
- : `Reset Order for all ${data.length} Documents`
252
- }
253
- /> */}
254
295
  </FloatingCard>
255
296
  )
256
297
  }
@@ -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
@@ -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}>
@@ -297,37 +390,7 @@ export default function WorkflowTool(props: WorkflowToolProps) {
297
390
  isDropDisabled={isDropDisabled}
298
391
  // props required for virtualization
299
392
  mode="virtual"
300
- // TODO: Render this as a memo/callback
301
- renderClone={(provided, snapshot, rubric) => {
302
- const item = data.find(
303
- (doc) =>
304
- doc?._metadata?.documentId === rubric.draggableId
305
- )
306
-
307
- return (
308
- <div
309
- {...provided.draggableProps}
310
- {...provided.dragHandleProps}
311
- ref={provided.innerRef}
312
- >
313
- {item ? (
314
- <DocumentCard
315
- isDragDisabled={false}
316
- userRoleCanDrop={userRoleCanDrop}
317
- isDragging={snapshot.isDragging}
318
- item={item}
319
- states={states}
320
- toggleInvalidDocumentId={
321
- toggleInvalidDocumentId
322
- }
323
- userList={userList}
324
- />
325
- ) : (
326
- <Feedback title="Item not found" tone="caution" />
327
- )}
328
- </div>
329
- )
330
- }}
393
+ renderClone={Clone}
331
394
  >
332
395
  {(provided, snapshot) => (
333
396
  <Card
@@ -349,6 +412,7 @@ export default function WorkflowTool(props: WorkflowToolProps) {
349
412
  <DocumentList
350
413
  data={data}
351
414
  invalidDocumentIds={invalidDocumentIds}
415
+ patchingIds={patchingIds}
352
416
  selectedSchemaTypes={selectedSchemaTypes}
353
417
  selectedUserIds={selectedUserIds}
354
418
  state={state}