sanity-plugin-workflow 1.0.0-beta.3 → 1.0.0-beta.5

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.3",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "A demonstration of a custom content publishing workflow using Sanity.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -49,13 +49,14 @@
49
49
  "watch": "pkg-utils watch --strict"
50
50
  },
51
51
  "dependencies": {
52
+ "@hello-pangea/dnd": "^16.2.0",
52
53
  "@sanity/icons": "^2.2.2",
53
54
  "@sanity/incompatible-plugin": "^1.0.4",
55
+ "@tanstack/react-virtual": "^3.0.0-beta.54",
54
56
  "@types/styled-components": "^5.1.26",
55
57
  "framer-motion": "^10.6.1",
56
58
  "groq": "^3.3.1",
57
59
  "lexorank": "^1.0.5",
58
- "react-beautiful-dnd": "^13.1.1",
59
60
  "react-fast-compare": "^3.2.1",
60
61
  "sanity-plugin-utils": "^1.3.0"
61
62
  },
@@ -66,7 +67,6 @@
66
67
  "@sanity/plugin-kit": "^3.1.4",
67
68
  "@sanity/semantic-release-preset": "^4.0.1",
68
69
  "@types/react": "^18.0.27",
69
- "@types/react-beautiful-dnd": "^13.1.2",
70
70
  "@typescript-eslint/eslint-plugin": "^5.51.0",
71
71
  "@typescript-eslint/parser": "^5.51.0",
72
72
  "eslint": "^8.33.0",
@@ -75,6 +75,7 @@
75
75
  "eslint-plugin-prettier": "^4.2.1",
76
76
  "eslint-plugin-react": "^7.32.2",
77
77
  "eslint-plugin-react-hooks": "^4.6.0",
78
+ "eslint-plugin-simple-import-sort": "^10.0.0",
78
79
  "husky": "^8.0.3",
79
80
  "lint-staged": "^13.2.0",
80
81
  "npm-run-all": "^4.1.5",
@@ -1,25 +1,20 @@
1
- // import {useState} from 'react'
2
- import {ArrowRightIcon, ArrowLeftIcon} from '@sanity/icons'
1
+ import {ArrowLeftIcon, ArrowRightIcon} from '@sanity/icons'
3
2
  import {useToast} from '@sanity/ui'
4
3
  import {useCurrentUser, useValidationStatus} from 'sanity'
5
4
  import {DocumentActionProps, useClient} from 'sanity'
6
5
 
7
- import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
8
6
  import {API_VERSION} from '../constants'
9
- import {State} from '../types'
10
7
  import {arraysContainMatchingString} from '../helpers/arraysContainMatchingString'
8
+ import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
9
+ import {State} from '../types'
11
10
 
11
+ // eslint-disable-next-line complexity
12
12
  export function UpdateWorkflow(
13
13
  props: DocumentActionProps,
14
14
  allStates: State[],
15
15
  actionState: State
16
16
  ) {
17
17
  const {id, type} = props
18
- const {validation, isValidating} = useValidationStatus(id, type)
19
- const hasValidationErrors =
20
- !isValidating &&
21
- validation?.length > 0 &&
22
- validation.find((v) => v.level === 'error')
23
18
 
24
19
  const user = useCurrentUser()
25
20
  const client = useClient({apiVersion: API_VERSION})
@@ -30,6 +25,13 @@ export function UpdateWorkflow(
30
25
  const {state: currentState} = data
31
26
  const {assignees = []} = data?.metadata ?? {}
32
27
 
28
+ const {validation, isValidating} = useValidationStatus(id, type)
29
+ const hasValidationErrors =
30
+ currentState?.requireValidation &&
31
+ !isValidating &&
32
+ validation?.length > 0 &&
33
+ validation.find((v) => v.level === 'error')
34
+
33
35
  if (error) {
34
36
  console.error(error)
35
37
  }
@@ -80,8 +82,6 @@ export function UpdateWorkflow(
80
82
  const DirectionIcon = direction === 'promote' ? ArrowRightIcon : ArrowLeftIcon
81
83
  const directionLabel = direction === 'promote' ? 'Promote' : 'Demote'
82
84
 
83
- let title = `${directionLabel} State to "${actionState.title}"`
84
-
85
85
  const userRoleCanUpdateState =
86
86
  user?.roles?.length && actionState?.roles?.length
87
87
  ? // If the Action state is limited to specific roles
@@ -93,10 +93,6 @@ export function UpdateWorkflow(
93
93
  : // No roles specified on the next state, so anyone can update
94
94
  actionState?.roles?.length !== 0
95
95
 
96
- if (!userRoleCanUpdateState) {
97
- title = `Your User role cannot ${directionLabel} State to "${actionState.title}"`
98
- }
99
-
100
96
  const actionStateIsAValidTransition =
101
97
  currentState?.id && currentState.transitions.length
102
98
  ? // If the Current State limits transitions to specific States
@@ -105,10 +101,6 @@ export function UpdateWorkflow(
105
101
  : // Otherwise this isn't a problem
106
102
  true
107
103
 
108
- if (!actionStateIsAValidTransition) {
109
- title = `You cannot ${directionLabel} State to "${actionState.title}" from "${currentState?.title}"`
110
- }
111
-
112
104
  const userAssignmentCanUpdateState = actionState.requireAssignment
113
105
  ? // If the Action State requires assigned users
114
106
  // Check the current user ID is in the assignees array
@@ -116,11 +108,17 @@ export function UpdateWorkflow(
116
108
  : // Otherwise this isn't a problem
117
109
  true
118
110
 
119
- if (!userAssignmentCanUpdateState) {
120
- title = `You must be assigned to the document to ${directionLabel} State to "${actionState.title}"`
121
- }
111
+ let title = `${directionLabel} State to "${actionState.title}"`
122
112
 
123
- if (hasValidationErrors) {
113
+ if (!userRoleCanUpdateState) {
114
+ title = `Your User role cannot ${directionLabel} State to "${actionState.title}"`
115
+ } else if (!actionStateIsAValidTransition) {
116
+ title = `You cannot ${directionLabel} State to "${actionState.title}" from "${currentState?.title}"`
117
+ } else if (!userAssignmentCanUpdateState) {
118
+ title = `You must be assigned to the document to ${directionLabel} State to "${actionState.title}"`
119
+ } else if (currentState?.requireValidation && isValidating) {
120
+ title = `Document is validating, cannot ${directionLabel} State to "${actionState.title}"`
121
+ } else if (hasValidationErrors) {
124
122
  title = `Document has validation errors, cannot ${directionLabel} State to "${actionState.title}"`
125
123
  }
126
124
 
@@ -129,7 +127,7 @@ export function UpdateWorkflow(
129
127
  disabled:
130
128
  loading ||
131
129
  error ||
132
- isValidating ||
130
+ (currentState?.requireValidation && isValidating) ||
133
131
  hasValidationErrors ||
134
132
  !currentState ||
135
133
  !userRoleCanUpdateState ||
@@ -0,0 +1,21 @@
1
+ import {useEffect} from 'react'
2
+ import {useValidationStatus, ValidationStatus} from 'sanity'
3
+
4
+ type ValidateProps = {
5
+ documentId: string
6
+ type: string
7
+ onChange: (validation: ValidationStatus) => void
8
+ }
9
+
10
+ // Document validation is siloed into its own component
11
+ // Because it's not performant to run on a lot of documents
12
+ export default function Validate(props: ValidateProps) {
13
+ const {documentId, type, onChange} = props
14
+ const {isValidating, validation = []} = useValidationStatus(documentId, type)
15
+
16
+ useEffect(() => {
17
+ onChange({isValidating, validation})
18
+ }, [onChange, isValidating, validation])
19
+
20
+ return null
21
+ }
@@ -1,11 +1,13 @@
1
1
  import {PublishIcon} from '@sanity/icons'
2
2
  import {PreviewValue, SanityDocument} from '@sanity/types'
3
3
  import {Box, Text, Tooltip} from '@sanity/ui'
4
+ import {TextWithTone} from 'sanity'
4
5
 
5
6
  import {TimeAgo} from './TimeAgo'
6
- import {TextWithTone} from 'sanity'
7
7
 
8
- export function PublishedStatus(props: {document?: PreviewValue | Partial<SanityDocument> | null}) {
8
+ export function PublishedStatus(props: {
9
+ document?: PreviewValue | Partial<SanityDocument> | null
10
+ }) {
9
11
  const {document} = props
10
12
  const updatedAt = document && '_updatedAt' in document && document._updatedAt
11
13
 
@@ -24,7 +26,12 @@ export function PublishedStatus(props: {document?: PreviewValue | Partial<Sanity
24
26
  </Box>
25
27
  }
26
28
  >
27
- <TextWithTone tone="positive" dimmed={!document} muted={!document} size={1}>
29
+ <TextWithTone
30
+ tone="positive"
31
+ dimmed={!document}
32
+ muted={!document}
33
+ size={1}
34
+ >
28
35
  <PublishIcon />
29
36
  </TextWithTone>
30
37
  </Tooltip>
@@ -1,17 +1,22 @@
1
1
  /* eslint-disable react/prop-types */
2
- import {useEffect, useMemo} from 'react'
3
- import {Box, Card, CardTone, Flex, Stack, useTheme} from '@sanity/ui'
4
2
  import {DragHandleIcon} from '@sanity/icons'
5
- import {useSchema, SchemaType, useValidationStatus} from 'sanity'
3
+ import {Box, Card, CardTone, Flex, Stack, useTheme} from '@sanity/ui'
4
+ import {useCallback, useEffect, useMemo, useState} from 'react'
5
+ import {
6
+ SchemaType,
7
+ useSchema,
8
+ ValidationStatus as ValidationStatusType,
9
+ } from 'sanity'
6
10
  import {Preview} from 'sanity'
7
11
 
8
- import EditButton from './EditButton'
9
12
  import {SanityDocumentWithMetadata, State, User} from '../../types'
10
13
  import UserDisplay from '../UserDisplay'
14
+ import CompleteButton from './CompleteButton'
11
15
  import {DraftStatus} from './core/DraftStatus'
12
16
  import {PublishedStatus} from './core/PublishedStatus'
17
+ import EditButton from './EditButton'
18
+ import Validate from './Validate'
13
19
  import {ValidationStatus} from './ValidationStatus'
14
- import CompleteButton from './CompleteButton'
15
20
 
16
21
  type DocumentCardProps = {
17
22
  isDragDisabled: boolean
@@ -38,6 +43,7 @@ export function DocumentCard(props: DocumentCardProps) {
38
43
  } = props
39
44
  const {assignees = [], documentId} = item._metadata ?? {}
40
45
  const schema = useSchema()
46
+ const state = states.find((s) => s.id === item._metadata?.state)
41
47
 
42
48
  // Perform document operations after State changes
43
49
  // If State has changed and the document needs to be un/published
@@ -80,10 +86,21 @@ export function DocumentCard(props: DocumentCardProps) {
80
86
 
81
87
  const isDarkMode = useTheme().sanity.color.dark
82
88
  const defaultCardTone = isDarkMode ? `transparent` : `default`
83
- const {validation = [], isValidating} = useValidationStatus(
84
- documentId ?? ``,
85
- item._type
86
- )
89
+
90
+ // Validation only runs if the state requests it
91
+ // Because it's not performant to run it on many documents simultaneously
92
+ // So we fake it here, and maybe set it inside <Validate />
93
+ const [optimisticValidation, setOptimisticValidation] =
94
+ useState<ValidationStatusType>({
95
+ isValidating: state?.requireValidation ?? false,
96
+ validation: [],
97
+ })
98
+
99
+ const {isValidating, validation} = optimisticValidation
100
+
101
+ const handleValidation = useCallback((updates: ValidationStatusType) => {
102
+ setOptimisticValidation(updates)
103
+ }, [])
87
104
 
88
105
  const cardTone = useMemo(() => {
89
106
  let tone: CardTone = defaultCardTone
@@ -92,7 +109,7 @@ export function DocumentCard(props: DocumentCardProps) {
92
109
  if (!documentId) return tone
93
110
  if (isDragging) tone = `positive`
94
111
 
95
- if (!isValidating && validation.length > 0) {
112
+ if (state?.requireValidation && !isValidating && validation.length > 0) {
96
113
  if (validation.some((v) => v.level === 'error')) {
97
114
  tone = `critical`
98
115
  } else {
@@ -102,13 +119,14 @@ export function DocumentCard(props: DocumentCardProps) {
102
119
 
103
120
  return tone
104
121
  }, [
105
- isDarkMode,
106
- userRoleCanDrop,
107
122
  defaultCardTone,
123
+ userRoleCanDrop,
124
+ isDarkMode,
108
125
  documentId,
109
126
  isDragging,
110
- validation,
111
127
  isValidating,
128
+ validation,
129
+ state?.requireValidation,
112
130
  ])
113
131
 
114
132
  // Update validation status
@@ -136,63 +154,72 @@ export function DocumentCard(props: DocumentCardProps) {
136
154
  )
137
155
 
138
156
  return (
139
- <Box paddingBottom={3} paddingX={3}>
140
- <Card radius={2} shadow={isDragging ? 3 : 1} tone={cardTone}>
141
- <Stack>
142
- <Card
143
- borderBottom
144
- radius={2}
145
- padding={3}
146
- paddingLeft={2}
147
- tone={cardTone}
148
- style={{pointerEvents: 'none'}}
149
- >
150
- <Flex align="center" justify="space-between" gap={1}>
151
- <Box flex={1}>
152
- <Preview
153
- layout="default"
154
- value={item}
155
- schemaType={schema.get(item._type) as SchemaType}
157
+ <>
158
+ {state?.requireValidation ? (
159
+ <Validate
160
+ documentId={documentId}
161
+ type={item._type}
162
+ onChange={handleValidation}
163
+ />
164
+ ) : null}
165
+ <Box paddingBottom={3} paddingX={3}>
166
+ <Card radius={2} shadow={isDragging ? 3 : 1} tone={cardTone}>
167
+ <Stack>
168
+ <Card
169
+ borderBottom
170
+ radius={2}
171
+ padding={3}
172
+ paddingLeft={2}
173
+ tone={cardTone}
174
+ style={{pointerEvents: 'none'}}
175
+ >
176
+ <Flex align="center" justify="space-between" gap={1}>
177
+ <Box flex={1}>
178
+ <Preview
179
+ layout="default"
180
+ value={item}
181
+ schemaType={schema.get(item._type) as SchemaType}
182
+ />
183
+ </Box>
184
+ <Box style={{flexShrink: 0}}>
185
+ {hasError || isDragDisabled ? null : <DragHandleIcon />}
186
+ </Box>
187
+ </Flex>
188
+ </Card>
189
+
190
+ <Card padding={2} radius={2} tone="inherit">
191
+ <Flex align="center" justify="space-between" gap={3}>
192
+ <Box flex={1}>
193
+ {documentId && (
194
+ <UserDisplay
195
+ userList={userList}
196
+ assignees={assignees}
197
+ documentId={documentId}
198
+ disabled={!userRoleCanDrop}
199
+ />
200
+ )}
201
+ </Box>
202
+ {validation.length > 0 ? (
203
+ <ValidationStatus validation={validation} />
204
+ ) : null}
205
+ <DraftStatus document={item} />
206
+ <PublishedStatus document={item} />
207
+ <EditButton
208
+ id={item._id}
209
+ type={item._type}
210
+ disabled={!userRoleCanDrop}
156
211
  />
157
- </Box>
158
- <Box style={{flexShrink: 0}}>
159
- {hasError || isDragDisabled ? null : <DragHandleIcon />}
160
- </Box>
161
- </Flex>
162
- </Card>
163
-
164
- <Card padding={2} radius={2} tone="inherit">
165
- <Flex align="center" justify="space-between" gap={3}>
166
- <Box flex={1}>
167
- {documentId && (
168
- <UserDisplay
169
- userList={userList}
170
- assignees={assignees}
212
+ {isLastState ? (
213
+ <CompleteButton
171
214
  documentId={documentId}
172
215
  disabled={!userRoleCanDrop}
173
216
  />
174
- )}
175
- </Box>
176
- {validation.length > 0 ? (
177
- <ValidationStatus validation={validation} />
178
- ) : null}
179
- <DraftStatus document={item} />
180
- <PublishedStatus document={item} />
181
- <EditButton
182
- id={item._id}
183
- type={item._type}
184
- disabled={!userRoleCanDrop}
185
- />
186
- {isLastState ? (
187
- <CompleteButton
188
- documentId={documentId}
189
- disabled={!userRoleCanDrop}
190
- />
191
- ) : null}
192
- </Flex>
193
- </Card>
194
- </Stack>
195
- </Card>
196
- </Box>
217
+ ) : null}
218
+ </Flex>
219
+ </Card>
220
+ </Stack>
221
+ </Card>
222
+ </Box>
223
+ </>
197
224
  )
198
225
  }
@@ -0,0 +1,122 @@
1
+ import {Draggable} from '@hello-pangea/dnd'
2
+ import {useVirtualizer} from '@tanstack/react-virtual'
3
+ import {useMemo, useRef} from 'react'
4
+ import {CurrentUser} from 'sanity'
5
+ import {UserExtended} from 'sanity-plugin-utils'
6
+
7
+ import {filterItemsAndSort} from '../helpers/filterItemsAndSort'
8
+ import {SanityDocumentWithMetadata, State} from '../types'
9
+ import {DocumentCard} from './DocumentCard'
10
+
11
+ type DocumentListProps = {
12
+ data: SanityDocumentWithMetadata[]
13
+ invalidDocumentIds: string[]
14
+ selectedSchemaTypes: string[]
15
+ selectedUserIds: string[]
16
+ state: State
17
+ states: State[]
18
+ toggleInvalidDocumentId: (
19
+ documentId: string,
20
+ action: 'ADD' | 'REMOVE'
21
+ ) => void
22
+ user: CurrentUser | null
23
+ userList: UserExtended[]
24
+ userRoleCanDrop: boolean
25
+ }
26
+
27
+ export default function DocumentList(props: DocumentListProps) {
28
+ const {
29
+ data = [],
30
+ invalidDocumentIds,
31
+ selectedSchemaTypes,
32
+ selectedUserIds,
33
+ state,
34
+ states,
35
+ toggleInvalidDocumentId,
36
+ user,
37
+ userList,
38
+ userRoleCanDrop,
39
+ } = props
40
+
41
+ const dataFiltered = useMemo(() => {
42
+ return data.length
43
+ ? filterItemsAndSort(data, state.id, selectedUserIds, selectedSchemaTypes)
44
+ : []
45
+ }, [data, selectedSchemaTypes, selectedUserIds, state.id])
46
+
47
+ const parentRef = useRef(null)
48
+
49
+ const rowVirtualizer = useVirtualizer({
50
+ count: data.length,
51
+ getScrollElement: () => parentRef.current,
52
+ getItemKey: (index) => dataFiltered[index]?._metadata?.documentId ?? index,
53
+ estimateSize: () => 113,
54
+ overscan: 5,
55
+ })
56
+
57
+ if (!data.length) {
58
+ return null
59
+ }
60
+
61
+ return (
62
+ <div
63
+ ref={parentRef}
64
+ style={{
65
+ height: `100%`,
66
+ overflow: 'auto',
67
+ paddingTop: 1,
68
+ // Smooths scrollbar behaviour
69
+ overflowAnchor: 'none',
70
+ scrollBehavior: 'auto',
71
+ }}
72
+ >
73
+ {/* {dataFiltered.map((item, itemIndex) => { */}
74
+ {rowVirtualizer.getVirtualItems().map((virtualItem) => {
75
+ const item = dataFiltered[virtualItem.index]
76
+
77
+ const {documentId, assignees} = item?._metadata ?? {}
78
+
79
+ if (!documentId) {
80
+ return null
81
+ }
82
+
83
+ const isInvalid = invalidDocumentIds.includes(documentId)
84
+ const meInAssignees = user?.id ? assignees?.includes(user.id) : false
85
+ const isDragDisabled =
86
+ !userRoleCanDrop ||
87
+ isInvalid ||
88
+ !(state.requireAssignment
89
+ ? state.requireAssignment && meInAssignees
90
+ : true)
91
+
92
+ return (
93
+ <Draggable
94
+ // The metadata's documentId is always the published one to avoid rerendering
95
+ key={documentId}
96
+ draggableId={documentId}
97
+ index={virtualItem.index}
98
+ isDragDisabled={isDragDisabled}
99
+ >
100
+ {(draggableProvided, draggableSnapshot) => (
101
+ <div
102
+ ref={draggableProvided.innerRef}
103
+ {...draggableProvided.draggableProps}
104
+ {...draggableProvided.dragHandleProps}
105
+ >
106
+ <DocumentCard
107
+ userRoleCanDrop={userRoleCanDrop}
108
+ isDragDisabled={isDragDisabled}
109
+ isDragging={draggableSnapshot.isDragging}
110
+ item={item}
111
+ toggleInvalidDocumentId={toggleInvalidDocumentId}
112
+ userList={userList}
113
+ states={states}
114
+ />
115
+ </div>
116
+ )}
117
+ </Draggable>
118
+ )
119
+ })}
120
+ </div>
121
+ )
122
+ }
@@ -1,8 +1,8 @@
1
- import {MenuButton, Menu, Flex, Card, Button} from '@sanity/ui'
1
+ import {ResetIcon, UserIcon} from '@sanity/icons'
2
+ import {Button, Card, Flex, Menu, MenuButton} from '@sanity/ui'
3
+ import {useCallback} from 'react'
2
4
  import {useCurrentUser, UserAvatar, useSchema} from 'sanity'
3
- import {UserIcon, ResetIcon} from '@sanity/icons'
4
5
  import {UserExtended, UserSelectMenu} from 'sanity-plugin-utils'
5
- import {useCallback} from 'react'
6
6
 
7
7
  type FiltersProps = {
8
8
  uniqueAssignedUsers: UserExtended[]
@@ -1,20 +1,24 @@
1
+ import {Button, useToast} from '@sanity/ui'
2
+ import {LexoRank} from 'lexorank'
1
3
  import React from 'react'
2
- import {useToast, Button} from '@sanity/ui'
3
4
  import {useClient} from 'sanity'
4
5
  import {UserExtended} from 'sanity-plugin-utils'
5
- import {LexoRank} from 'lexorank'
6
6
 
7
- import FloatingCard from './FloatingCard'
8
7
  import {API_VERSION} from '../constants'
9
8
  import {SanityDocumentWithMetadata, State} from '../types'
9
+ import FloatingCard from './FloatingCard'
10
10
 
11
- type ValidatorsProps = {
11
+ type VerifyProps = {
12
12
  data: SanityDocumentWithMetadata[]
13
13
  userList: UserExtended[]
14
14
  states: State[]
15
15
  }
16
16
 
17
- export default function Validators({data, userList, states}: ValidatorsProps) {
17
+ // This component checks the validity of the data in the Kanban
18
+ // It will only render something it there is invalid date
19
+ // And will render buttons to fix the data
20
+ export default function Verify(props: VerifyProps) {
21
+ const {data, userList, states} = props
18
22
  const client = useClient({apiVersion: API_VERSION})
19
23
  const toast = useToast()
20
24
 
@@ -143,6 +147,32 @@ export default function Validators({data, userList, states}: ValidatorsProps) {
143
147
  [data, client, toast]
144
148
  )
145
149
 
150
+ // A document could be deleted and the workflow metadata left behind
151
+ const orphanedMetadataDocumentIds = React.useMemo(() => {
152
+ return data.length
153
+ ? data.filter((doc) => !doc?._id).map((doc) => doc._metadata.documentId)
154
+ : []
155
+ }, [data])
156
+
157
+ const handleOrphans = React.useCallback(() => {
158
+ toast.push({
159
+ title: 'Removing orphaned metadata...',
160
+ status: 'info',
161
+ })
162
+
163
+ const tx = client.transaction()
164
+ orphanedMetadataDocumentIds.forEach((id) => {
165
+ tx.delete(`workflow-metadata.${id}`)
166
+ })
167
+
168
+ tx.commit()
169
+
170
+ toast.push({
171
+ title: `Removed ${orphanedMetadataDocumentIds.length} orphaned metadata documents`,
172
+ status: 'success',
173
+ })
174
+ }, [client, orphanedMetadataDocumentIds, toast])
175
+
146
176
  return (
147
177
  <FloatingCard>
148
178
  {documentsWithoutValidMetadataIds.length > 0 ? (
@@ -178,6 +208,13 @@ export default function Validators({data, userList, states}: ValidatorsProps) {
178
208
  }
179
209
  />
180
210
  ) : null}
211
+ {orphanedMetadataDocumentIds.length > 0 ? (
212
+ <Button
213
+ text="Cleanup orphaned metadata"
214
+ onClick={handleOrphans}
215
+ tone="caution"
216
+ />
217
+ ) : null}
181
218
  {/* <Button
182
219
  tone="caution"
183
220
  onClick={() =>