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
@@ -0,0 +1,78 @@
1
+ import {InfoOutlineIcon, UserIcon} from '@sanity/icons'
2
+ import {Badge, BadgeTone, Box, Card, Flex, Text} from '@sanity/ui'
3
+ import styled, {css} from 'styled-components'
4
+
5
+ import {State} from '../../types'
6
+ import {Status} from './Status'
7
+
8
+ const StyledStickyCard = styled(Card)(
9
+ () => css`
10
+ position: sticky;
11
+ top: 0;
12
+ z-index: 1;
13
+ `
14
+ )
15
+
16
+ type StateTitleProps = {
17
+ state: State
18
+ requireAssignment: boolean
19
+ userRoleCanDrop: boolean
20
+ isDropDisabled: boolean
21
+ draggingFrom: string
22
+ documentCount: number
23
+ }
24
+
25
+ export default function StateTitle(props: StateTitleProps) {
26
+ const {
27
+ state,
28
+ requireAssignment,
29
+ userRoleCanDrop,
30
+ isDropDisabled,
31
+ draggingFrom,
32
+ documentCount,
33
+ } = props
34
+
35
+ let tone: BadgeTone = 'default'
36
+ const isSource = draggingFrom === state.id
37
+
38
+ if (draggingFrom) {
39
+ tone = isDropDisabled || isSource ? 'default' : 'positive'
40
+ }
41
+
42
+ return (
43
+ <StyledStickyCard paddingY={4} padding={3} tone="inherit">
44
+ <Flex gap={3} align="center">
45
+ <Badge
46
+ mode={
47
+ (draggingFrom && !isDropDisabled) || isSource
48
+ ? 'default'
49
+ : 'outline'
50
+ }
51
+ tone={tone}
52
+ muted={!userRoleCanDrop || isDropDisabled}
53
+ >
54
+ {state.title}
55
+ </Badge>
56
+ {userRoleCanDrop ? null : (
57
+ <Status
58
+ text="You do not have permissions to move documents to this State"
59
+ icon={InfoOutlineIcon}
60
+ />
61
+ )}
62
+ {requireAssignment ? (
63
+ <Status
64
+ text="You must be assigned to the document to move documents to this State"
65
+ icon={UserIcon}
66
+ />
67
+ ) : null}
68
+ <Box flex={1}>
69
+ {documentCount > 0 ? (
70
+ <Text weight="semibold" align="right" size={1}>
71
+ {documentCount}
72
+ </Text>
73
+ ) : null}
74
+ </Box>
75
+ </Flex>
76
+ </StyledStickyCard>
77
+ )
78
+ }
@@ -1,11 +1,10 @@
1
1
  import React from 'react'
2
- import {Button, Popover, useToast} from '@sanity/ui'
3
- import {AddIcon} from '@sanity/icons'
2
+ import {useToast} from '@sanity/ui'
4
3
  import {UserSelectMenu} from 'sanity-plugin-utils'
5
4
  import {useClient} from 'sanity'
6
5
 
7
- import AvatarGroup from './DocumentCard/AvatarGroup'
8
6
  import {User} from '../types'
7
+ import {API_VERSION} from '../constants'
9
8
 
10
9
  type UserAssignmentProps = {
11
10
  userList: User[]
@@ -15,28 +14,28 @@ type UserAssignmentProps = {
15
14
 
16
15
  export default function UserAssignment(props: UserAssignmentProps) {
17
16
  const {assignees, userList, documentId} = props
18
- const client = useClient()
17
+ const client = useClient({apiVersion: API_VERSION})
19
18
  const toast = useToast()
20
- const [openId, setOpenId] = React.useState<string>(``)
21
19
 
22
20
  const addAssignee = React.useCallback(
23
21
  (userId: string) => {
24
- if (!userId) {
22
+ const user = userList.find((u) => u.id === userId)
23
+
24
+ if (!userId || !user) {
25
25
  return toast.push({
26
26
  status: 'error',
27
- title: 'No user selected',
27
+ title: 'Could not find User',
28
28
  })
29
29
  }
30
30
 
31
- client
31
+ return client
32
32
  .patch(`workflow-metadata.${documentId}`)
33
33
  .setIfMissing({assignees: []})
34
34
  .insert(`after`, `assignees[-1]`, [userId])
35
35
  .commit()
36
36
  .then(() => {
37
37
  return toast.push({
38
- title: `Assigned user to document`,
39
- description: userId,
38
+ title: `Added ${user.displayName} to assignees`,
40
39
  status: 'success',
41
40
  })
42
41
  })
@@ -50,90 +49,73 @@ export default function UserAssignment(props: UserAssignmentProps) {
50
49
  })
51
50
  })
52
51
  },
53
- [documentId, client, toast]
52
+ [documentId, client, toast, userList]
54
53
  )
55
54
 
56
55
  const removeAssignee = React.useCallback(
57
- (id: string, userId: string) => {
58
- client
59
- .patch(`workflow-metadata.${id}`)
56
+ (userId: string) => {
57
+ const user = userList.find((u) => u.id === userId)
58
+
59
+ if (!userId || !user) {
60
+ return toast.push({
61
+ status: 'error',
62
+ title: 'Could not find User',
63
+ })
64
+ }
65
+
66
+ return client
67
+ .patch(`workflow-metadata.${documentId}`)
60
68
  .unset([`assignees[@ == "${userId}"]`])
61
69
  .commit()
62
- .then((res) => res)
70
+ .then(() => {
71
+ return toast.push({
72
+ title: `Removed ${user.displayName} from assignees`,
73
+ status: 'success',
74
+ })
75
+ })
63
76
  .catch((err) => {
64
77
  console.error(err)
65
78
 
66
79
  return toast.push({
67
80
  title: `Failed to remove assignee`,
68
- description: id,
81
+ description: documentId,
69
82
  status: 'error',
70
83
  })
71
84
  })
72
85
  },
73
- [client, toast]
86
+ [client, toast, documentId, userList]
74
87
  )
75
88
 
76
- const clearAssignees = React.useCallback(
77
- (id: string) => {
78
- client
79
- .patch(`workflow-metadata.${id}`)
80
- .unset([`assignees`])
81
- .commit()
82
- .then((res) => res)
83
- .catch((err) => {
84
- console.error(err)
89
+ const clearAssignees = React.useCallback(() => {
90
+ return client
91
+ .patch(`workflow-metadata.${documentId}`)
92
+ .unset([`assignees`])
93
+ .commit()
94
+ .then(() => {
95
+ return toast.push({
96
+ title: `Cleared assignees`,
97
+ status: 'success',
98
+ })
99
+ })
100
+ .catch((err) => {
101
+ console.error(err)
85
102
 
86
- return toast.push({
87
- title: `Failed to clear assignees`,
88
- description: id,
89
- status: 'error',
90
- })
103
+ return toast.push({
104
+ title: `Failed to clear assignees`,
105
+ description: documentId,
106
+ status: 'error',
91
107
  })
92
- },
93
- [client, toast]
94
- )
108
+ })
109
+ }, [client, toast, documentId])
95
110
 
96
111
  return (
97
- <Popover
98
- // @ts-ignore
99
- // ref={setPopoverRef}
100
- // onKeyDown={handleKeyDown}
101
- content={
102
- <UserSelectMenu
103
- style={{maxHeight: 300}}
104
- value={assignees || []}
105
- userList={userList}
106
- onAdd={addAssignee}
107
- onClear={clearAssignees}
108
- onRemove={removeAssignee}
109
- open={openId === documentId}
110
- />
111
- }
112
- portal
113
- open={openId === documentId}
114
- >
115
- {!assignees || assignees.length === 0 ? (
116
- <Button
117
- onClick={() => setOpenId(documentId)}
118
- fontSize={1}
119
- padding={2}
120
- tabIndex={-1}
121
- icon={AddIcon}
122
- text="Assign"
123
- tone="positive"
124
- />
125
- ) : (
126
- <Button
127
- onClick={() => setOpenId(documentId)}
128
- padding={0}
129
- mode="bleed"
130
- style={{width: `100%`}}
131
- >
132
- <AvatarGroup
133
- users={userList.filter((u) => assignees.includes(u.id))}
134
- />
135
- </Button>
136
- )}
137
- </Popover>
112
+ <UserSelectMenu
113
+ style={{maxHeight: 300}}
114
+ value={assignees || []}
115
+ userList={userList}
116
+ onAdd={addAssignee}
117
+ onClear={clearAssignees}
118
+ onRemove={removeAssignee}
119
+ />
138
120
  )
139
121
  }
@@ -0,0 +1,27 @@
1
+ import {Card} from '@sanity/ui'
2
+ import {FunctionComponent} from 'react'
3
+ import {ArraySchemaType, ArrayOfPrimitivesInputProps, useFormValue} from 'sanity'
4
+ import {useProjectUsers} from 'sanity-plugin-utils'
5
+
6
+ import {API_VERSION} from '../constants'
7
+ import UserAssignment from './UserAssignment'
8
+
9
+ const UserAssignmentInput: FunctionComponent<
10
+ ArrayOfPrimitivesInputProps<string | number | boolean, ArraySchemaType>
11
+ > = (props) => {
12
+ const documentId = useFormValue([`documentId`])
13
+ const userList = useProjectUsers({apiVersion: API_VERSION})
14
+
15
+ const stringValue =
16
+ Array.isArray(props?.value) && props?.value?.length
17
+ ? props.value.map((item) => String(item))
18
+ : []
19
+
20
+ return (
21
+ <Card border padding={1}>
22
+ <UserAssignment userList={userList} assignees={stringValue} documentId={String(documentId)} />
23
+ </Card>
24
+ )
25
+ }
26
+
27
+ export default UserAssignmentInput
@@ -0,0 +1,57 @@
1
+ import React from 'react'
2
+ import {Button, Grid, Popover, useClickOutside} from '@sanity/ui'
3
+ import {AddIcon} from '@sanity/icons'
4
+
5
+ import AvatarGroup from './DocumentCard/AvatarGroup'
6
+ import {User} from '../types'
7
+ import UserAssignment from './UserAssignment'
8
+
9
+ type UserDisplayProps = {
10
+ userList: User[]
11
+ assignees: string[]
12
+ documentId: string
13
+ disabled?: boolean
14
+ }
15
+
16
+ export default function UserDisplay(props: UserDisplayProps) {
17
+ const {assignees, userList, documentId, disabled = false} = props
18
+
19
+ const [button] = React.useState(null)
20
+ const [popover, setPopover] = React.useState(null)
21
+ const [isOpen, setIsOpen] = React.useState(false)
22
+
23
+ const close = React.useCallback(() => setIsOpen(false), [])
24
+ const open = React.useCallback(() => setIsOpen(true), [])
25
+
26
+ useClickOutside(close, [button, popover])
27
+
28
+ return (
29
+ <Popover
30
+ // @ts-ignore
31
+ ref={setPopover}
32
+ content={<UserAssignment userList={userList} assignees={assignees} documentId={documentId} />}
33
+ portal
34
+ open={isOpen}
35
+ >
36
+ {!assignees || assignees.length === 0 ? (
37
+ <Button
38
+ onClick={open}
39
+ fontSize={1}
40
+ padding={2}
41
+ tabIndex={-1}
42
+ icon={AddIcon}
43
+ text="Assign"
44
+ tone="positive"
45
+ mode="ghost"
46
+ disabled={disabled}
47
+ />
48
+ ) : (
49
+ <Grid>
50
+ <Button onClick={open} padding={0} mode="bleed" disabled={disabled}>
51
+ <AvatarGroup users={userList.filter((u) => assignees.includes(u.id))} />
52
+ </Button>
53
+ </Grid>
54
+ )}
55
+ </Popover>
56
+ )
57
+ }
@@ -0,0 +1,297 @@
1
+ import {Button, useToast} from '@sanity/ui'
2
+ import {LexoRank} from 'lexorank'
3
+ import React from 'react'
4
+ import {useClient} from 'sanity'
5
+ import {UserExtended} from 'sanity-plugin-utils'
6
+
7
+ import {API_VERSION} from '../constants'
8
+ import {generateMultipleOrderRanks} from '../helpers/generateMultipleOrderRanks'
9
+ import {SanityDocumentWithMetadata, State} from '../types'
10
+ import FloatingCard from './FloatingCard'
11
+
12
+ type VerifyProps = {
13
+ data: SanityDocumentWithMetadata[]
14
+ userList: UserExtended[]
15
+ states: State[]
16
+ }
17
+
18
+ // This component checks the validity of the data in the Kanban
19
+ // It will only render something it there is invalid date
20
+ // And will render buttons to fix the data
21
+ export default function Verify(props: VerifyProps) {
22
+ const {data, userList, states} = props
23
+ const client = useClient({apiVersion: API_VERSION})
24
+ const toast = useToast()
25
+
26
+ // A lot of error-checking
27
+ const documentsWithoutValidMetadataIds = data?.length
28
+ ? data.reduce((acc, cur) => {
29
+ const {documentId, state} = cur._metadata ?? {}
30
+ const stateExists = states.find((s) => s.id === state)
31
+
32
+ return !stateExists && documentId ? [...acc, documentId] : acc
33
+ }, [] as string[])
34
+ : []
35
+
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
43
+
44
+ return !allAssigneesExist && documentId ? [...acc, documentId] : acc
45
+ }, [] as string[])
46
+ : []
47
+
48
+ const documentsWithoutOrderIds = data?.length
49
+ ? data.reduce((acc, cur) => {
50
+ const {documentId, orderRank} = cur._metadata ?? {}
51
+
52
+ return !orderRank && documentId ? [...acc, documentId] : acc
53
+ }, [] as string[])
54
+ : []
55
+
56
+ const documentsWithDuplicatedOrderIds = data?.length
57
+ ? data.reduce((acc, cur) => {
58
+ const {documentId, orderRank} = cur._metadata ?? {}
59
+
60
+ return orderRank &&
61
+ data.filter((d) => d._metadata?.orderRank === orderRank).length > 1 &&
62
+ documentId
63
+ ? [...acc, documentId]
64
+ : acc
65
+ }, [] as string[])
66
+ : []
67
+
68
+ // Updates metadata documents to a valid, existing state
69
+ const correctDocuments = React.useCallback(
70
+ async (ids: string[]) => {
71
+ toast.push({
72
+ title: 'Correcting...',
73
+ status: 'info',
74
+ })
75
+
76
+ const tx = ids.reduce((item, documentId) => {
77
+ return item.patch(`workflow-metadata.${documentId}`, {
78
+ set: {state: states[0].id},
79
+ })
80
+ }, client.transaction())
81
+
82
+ await tx.commit()
83
+
84
+ toast.push({
85
+ title: `Corrected ${
86
+ ids.length === 1 ? `1 Document` : `${ids.length} Documents`
87
+ }`,
88
+ status: 'success',
89
+ })
90
+ },
91
+ [client, states, toast]
92
+ )
93
+
94
+ // Remove users that are no longer in the project from documents
95
+ const removeUsersFromDocuments = React.useCallback(
96
+ async (ids: string[]) => {
97
+ toast.push({
98
+ title: 'Removing users...',
99
+ status: 'info',
100
+ })
101
+
102
+ const tx = ids.reduce((item, documentId) => {
103
+ const {assignees} =
104
+ data.find((d) => d._id === documentId)?._metadata ?? {}
105
+ const validAssignees = assignees?.length
106
+ ? // eslint-disable-next-line max-nested-callbacks
107
+ assignees.filter((a) => userList.find((u) => u.id === a)?.id)
108
+ : []
109
+
110
+ return item.patch(`workflow-metadata.${documentId}`, {
111
+ set: {assignees: validAssignees},
112
+ })
113
+ }, client.transaction())
114
+
115
+ await tx.commit()
116
+
117
+ toast.push({
118
+ title: `Corrected ${
119
+ ids.length === 1 ? `1 Document` : `${ids.length} Documents`
120
+ }`,
121
+ status: 'success',
122
+ })
123
+ },
124
+ [client, data, toast, userList]
125
+ )
126
+
127
+ // Add order value to metadata documents
128
+ const addOrderToDocuments = React.useCallback(
129
+ async (ids: string[]) => {
130
+ toast.push({
131
+ title: 'Adding ordering...',
132
+ status: 'info',
133
+ })
134
+
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)
142
+
143
+ const tx = client.transaction()
144
+
145
+ // Create a new in-between value for each document
146
+ for (let index = 0; index < ids.length; index += 1) {
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)
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) {
178
+ tx.patch(`workflow-metadata.${ids[index]}`, {
179
+ set: {orderRank: ranks[index].toString()},
180
+ })
181
+ }
182
+
183
+ await tx.commit()
184
+
185
+ toast.push({
186
+ title: `Added order to ${
187
+ ids.length === 1 ? `1 Document` : `${ids.length} Documents`
188
+ }`,
189
+ status: 'success',
190
+ })
191
+ },
192
+ [data, client, toast]
193
+ )
194
+
195
+ // A document could be deleted and the workflow metadata left behind
196
+ const orphanedMetadataDocumentIds = React.useMemo(() => {
197
+ return data.length
198
+ ? data.filter((doc) => !doc?._id).map((doc) => doc._metadata.documentId)
199
+ : []
200
+ }, [data])
201
+
202
+ const handleOrphans = React.useCallback(() => {
203
+ toast.push({
204
+ title: 'Removing orphaned metadata...',
205
+ status: 'info',
206
+ })
207
+
208
+ const tx = client.transaction()
209
+ orphanedMetadataDocumentIds.forEach((id) => {
210
+ tx.delete(`workflow-metadata.${id}`)
211
+ })
212
+
213
+ tx.commit()
214
+
215
+ toast.push({
216
+ title: `Removed ${orphanedMetadataDocumentIds.length} orphaned metadata documents`,
217
+ status: 'success',
218
+ })
219
+ }, [client, orphanedMetadataDocumentIds, toast])
220
+
221
+ return (
222
+ <FloatingCard>
223
+ {documentsWithoutValidMetadataIds.length > 0 ? (
224
+ <Button
225
+ tone="caution"
226
+ mode="ghost"
227
+ onClick={() => correctDocuments(documentsWithoutValidMetadataIds)}
228
+ text={
229
+ documentsWithoutValidMetadataIds.length === 1
230
+ ? `Correct 1 Document State`
231
+ : `Correct ${documentsWithoutValidMetadataIds.length} Document States`
232
+ }
233
+ />
234
+ ) : null}
235
+ {documentsWithInvalidUserIds.length > 0 ? (
236
+ <Button
237
+ tone="caution"
238
+ mode="ghost"
239
+ onClick={() => removeUsersFromDocuments(documentsWithInvalidUserIds)}
240
+ text={
241
+ documentsWithInvalidUserIds.length === 1
242
+ ? `Remove Invalid Users from 1 Document`
243
+ : `Remove Invalid Users from ${documentsWithInvalidUserIds.length} Documents`
244
+ }
245
+ />
246
+ ) : null}
247
+ {documentsWithoutOrderIds.length > 0 ? (
248
+ <Button
249
+ tone="caution"
250
+ mode="ghost"
251
+ onClick={() => addOrderToDocuments(documentsWithoutOrderIds)}
252
+ text={
253
+ documentsWithoutOrderIds.length === 1
254
+ ? `Set Order for 1 Document`
255
+ : `Set Order for ${documentsWithoutOrderIds.length} Documents`
256
+ }
257
+ />
258
+ ) : null}
259
+ {documentsWithDuplicatedOrderIds.length > 0 ? (
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
+ </>
286
+ ) : null}
287
+ {orphanedMetadataDocumentIds.length > 0 ? (
288
+ <Button
289
+ text="Cleanup orphaned metadata"
290
+ onClick={handleOrphans}
291
+ tone="caution"
292
+ mode="ghost"
293
+ />
294
+ ) : null}
295
+ </FloatingCard>
296
+ )
297
+ }