sanity-plugin-workflow 1.0.0-beta.1 → 1.0.0-beta.10
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 +2106 -1
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +2119 -1
- package/lib/index.js.map +1 -1
- package/package.json +51 -40
- package/src/actions/AssignWorkflow.tsx +49 -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
|
@@ -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 {
|
|
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
|
-
|
|
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: '
|
|
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: `
|
|
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
|
-
(
|
|
58
|
-
|
|
59
|
-
|
|
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((
|
|
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:
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
})
|
|
103
|
+
return toast.push({
|
|
104
|
+
title: `Failed to clear assignees`,
|
|
105
|
+
description: documentId,
|
|
106
|
+
status: 'error',
|
|
91
107
|
})
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
108
|
+
})
|
|
109
|
+
}, [client, toast, documentId])
|
|
95
110
|
|
|
96
111
|
return (
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|