sanity-plugin-workflow 1.0.0-beta.1
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 +21 -0
- package/README.md +62 -0
- package/lib/index.esm.js +1 -0
- package/lib/index.esm.js.map +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -0
- package/lib/src/index.d.ts +19 -0
- package/package.json +95 -0
- package/sanity.json +8 -0
- package/src/actions/DemoteAction.tsx +62 -0
- package/src/actions/PromoteAction.tsx +62 -0
- package/src/actions/RequestReviewAction.js +61 -0
- package/src/actions/index.js +21 -0
- package/src/badges/index.tsx +31 -0
- package/src/components/DocumentCard/AvatarGroup.tsx +39 -0
- package/src/components/DocumentCard/EditButton.tsx +27 -0
- package/src/components/DocumentCard/index.tsx +92 -0
- package/src/components/Mutate.tsx +54 -0
- package/src/components/StateTimeline.tsx +98 -0
- package/src/components/UserAssignment.tsx +139 -0
- package/src/components/UserSelectInput.tsx +43 -0
- package/src/components/WorkflowTool.tsx +228 -0
- package/src/hooks/useWorkflowDocuments.tsx +166 -0
- package/src/hooks/useWorkflowMetadata.tsx +44 -0
- package/src/index.ts +94 -0
- package/src/schema/workflow/metadata.ts +38 -0
- package/src/types/index.ts +50 -0
- package/v2-incompatible.js +11 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Button,
|
|
5
|
+
Card,
|
|
6
|
+
Flex,
|
|
7
|
+
Popover,
|
|
8
|
+
Stack,
|
|
9
|
+
useClickOutside,
|
|
10
|
+
useTheme,
|
|
11
|
+
} from '@sanity/ui'
|
|
12
|
+
import {AddIcon, DragHandleIcon} from '@sanity/icons'
|
|
13
|
+
import React, {useState} from 'react'
|
|
14
|
+
import {useSchema, SchemaType} from 'sanity'
|
|
15
|
+
import {UserSelectMenu} from 'sanity-plugin-utils'
|
|
16
|
+
import {Preview} from 'sanity'
|
|
17
|
+
|
|
18
|
+
import EditButton from './EditButton'
|
|
19
|
+
import {SanityDocumentWithMetadata, User} from '../../types'
|
|
20
|
+
import AvatarGroup from './AvatarGroup'
|
|
21
|
+
import UserAssignment from '../UserAssignment'
|
|
22
|
+
|
|
23
|
+
type DocumentCardProps = {
|
|
24
|
+
userList: User[]
|
|
25
|
+
isDragging: boolean
|
|
26
|
+
item: SanityDocumentWithMetadata
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function DocumentCard(props: DocumentCardProps) {
|
|
30
|
+
const {userList, isDragging, item} = props
|
|
31
|
+
const {assignees = [], documentId} = item._metadata ?? {}
|
|
32
|
+
const schema = useSchema()
|
|
33
|
+
|
|
34
|
+
const isDarkMode = useTheme().sanity.color.dark
|
|
35
|
+
const defaultCardTone = isDarkMode ? 'transparent' : 'default'
|
|
36
|
+
|
|
37
|
+
// Open/close handler
|
|
38
|
+
// const [popoverRef, setPopoverRef] = useState(null)
|
|
39
|
+
// const [openId, setOpenId] = useState<string | undefined>(``)
|
|
40
|
+
|
|
41
|
+
// useClickOutside(() => setOpenId(``), [popoverRef])
|
|
42
|
+
|
|
43
|
+
// const handleKeyDown = React.useCallback((e) => {
|
|
44
|
+
// if (e.key === 'Escape') {
|
|
45
|
+
// setOpenId(``)
|
|
46
|
+
// }
|
|
47
|
+
// }, [])
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Box paddingY={2} paddingX={3}>
|
|
51
|
+
<Card
|
|
52
|
+
radius={2}
|
|
53
|
+
shadow={isDragging ? 3 : 1}
|
|
54
|
+
tone={isDragging ? 'positive' : defaultCardTone}
|
|
55
|
+
>
|
|
56
|
+
<Stack>
|
|
57
|
+
<Card
|
|
58
|
+
borderBottom
|
|
59
|
+
radius={2}
|
|
60
|
+
padding={3}
|
|
61
|
+
paddingLeft={2}
|
|
62
|
+
tone="inherit"
|
|
63
|
+
style={{pointerEvents: 'none'}}
|
|
64
|
+
>
|
|
65
|
+
<Flex align="center" justify="space-between" gap={1}>
|
|
66
|
+
<Preview
|
|
67
|
+
layout="default"
|
|
68
|
+
value={item}
|
|
69
|
+
schemaType={schema.get(item._type) as SchemaType}
|
|
70
|
+
/>
|
|
71
|
+
<DragHandleIcon style={{flexShrink: 0}} />
|
|
72
|
+
</Flex>
|
|
73
|
+
</Card>
|
|
74
|
+
|
|
75
|
+
<Card padding={2} radius={2} tone="inherit">
|
|
76
|
+
<Flex align="center" justify="space-between" gap={1}>
|
|
77
|
+
{documentId && (
|
|
78
|
+
<UserAssignment
|
|
79
|
+
userList={userList}
|
|
80
|
+
assignees={assignees}
|
|
81
|
+
documentId={documentId}
|
|
82
|
+
/>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
<EditButton id={item._id} type={item._type} />
|
|
86
|
+
</Flex>
|
|
87
|
+
</Card>
|
|
88
|
+
</Stack>
|
|
89
|
+
</Card>
|
|
90
|
+
</Box>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {Card, useToast} from '@sanity/ui'
|
|
3
|
+
import {useDocumentOperation} from 'sanity'
|
|
4
|
+
|
|
5
|
+
import {State} from '../types'
|
|
6
|
+
|
|
7
|
+
type MutateProps = {
|
|
8
|
+
_id: string
|
|
9
|
+
_type: string
|
|
10
|
+
state: State
|
|
11
|
+
documentId: string
|
|
12
|
+
onComplete: (id: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function Mutate(props: MutateProps) {
|
|
16
|
+
const {_id, _type, documentId, state, onComplete} = props
|
|
17
|
+
const ops = useDocumentOperation(documentId, _type)
|
|
18
|
+
const isDraft = _id.startsWith('drafts.')
|
|
19
|
+
|
|
20
|
+
const toast = useToast()
|
|
21
|
+
|
|
22
|
+
if (isDraft && state.operation === 'publish') {
|
|
23
|
+
if (!ops.publish.disabled) {
|
|
24
|
+
ops.publish.execute()
|
|
25
|
+
onComplete(_id)
|
|
26
|
+
toast.push({
|
|
27
|
+
title: 'Published Document',
|
|
28
|
+
description: documentId,
|
|
29
|
+
status: 'success',
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
} else if (!isDraft && state.operation === 'unpublish') {
|
|
33
|
+
if (!ops.unpublish.disabled) {
|
|
34
|
+
ops.unpublish.execute()
|
|
35
|
+
onComplete(_id)
|
|
36
|
+
toast.push({
|
|
37
|
+
title: 'Unpublished Document',
|
|
38
|
+
description: documentId,
|
|
39
|
+
status: 'success',
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
// Clean up if it's not going to un/publish
|
|
44
|
+
onComplete(_id)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// return null
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Card padding={3} shadow={2} tone="primary">
|
|
51
|
+
Mutating: {_id} to {state.title}
|
|
52
|
+
</Card>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import {Button, Card, Text, Inline, Stack, useToast} from '@sanity/ui'
|
|
2
|
+
import React, {useEffect} from 'react'
|
|
3
|
+
import {ObjectInputProps, useClient} from 'sanity'
|
|
4
|
+
|
|
5
|
+
import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
|
|
6
|
+
import {State} from '../types'
|
|
7
|
+
|
|
8
|
+
type StateTimelineProps = ObjectInputProps & {
|
|
9
|
+
states: State[]
|
|
10
|
+
children: React.ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function StateTimeline(props: StateTimelineProps) {
|
|
14
|
+
// return (
|
|
15
|
+
// <Stack space={3}>
|
|
16
|
+
// <StateTimeline {...props} states={states}>
|
|
17
|
+
// {props.renderDefault(props)}
|
|
18
|
+
// </StateTimeline>
|
|
19
|
+
// </Stack>
|
|
20
|
+
// )
|
|
21
|
+
console.log(props)
|
|
22
|
+
const {value, states, children} = props
|
|
23
|
+
|
|
24
|
+
const documentId = String(value?._id)
|
|
25
|
+
|
|
26
|
+
const {data, loading, error} = useWorkflowMetadata(documentId, states)
|
|
27
|
+
const {state} = data
|
|
28
|
+
const [mutatingToState, setMutatingToState] = React.useState<string | null>(
|
|
29
|
+
null
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const client = useClient()
|
|
33
|
+
const toast = useToast()
|
|
34
|
+
|
|
35
|
+
// Just because the document is patched ...
|
|
36
|
+
// doesn't mean the latest data has been returned from the listener
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (data) {
|
|
39
|
+
setMutatingToState(null)
|
|
40
|
+
}
|
|
41
|
+
}, [data])
|
|
42
|
+
|
|
43
|
+
const changeState = React.useCallback(
|
|
44
|
+
(publishedId: string, newState: State) => {
|
|
45
|
+
setMutatingToState(newState.id)
|
|
46
|
+
|
|
47
|
+
client
|
|
48
|
+
.patch(`workflow-metadata.${publishedId}`)
|
|
49
|
+
.set({state: newState.id})
|
|
50
|
+
.commit()
|
|
51
|
+
.then(() => {
|
|
52
|
+
toast.push({
|
|
53
|
+
status: 'success',
|
|
54
|
+
title: `Document moved to ${newState.title}`,
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
.catch((err) => {
|
|
58
|
+
console.error(err)
|
|
59
|
+
toast.push({
|
|
60
|
+
status: 'error',
|
|
61
|
+
title: `Document moved failed`,
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
},
|
|
65
|
+
[client, toast]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Stack space={3}>
|
|
70
|
+
<Text weight="medium" size={1}>
|
|
71
|
+
Workflow State
|
|
72
|
+
</Text>
|
|
73
|
+
<Card padding={1} radius={3} border tone="primary">
|
|
74
|
+
<Inline space={1}>
|
|
75
|
+
{states.map((s) => (
|
|
76
|
+
<Button
|
|
77
|
+
disabled={loading || error || Boolean(mutatingToState)}
|
|
78
|
+
fontSize={1}
|
|
79
|
+
tone="primary"
|
|
80
|
+
mode={
|
|
81
|
+
(!mutatingToState && s.id === state?.id) ||
|
|
82
|
+
s.id === mutatingToState
|
|
83
|
+
? `default`
|
|
84
|
+
: `ghost`
|
|
85
|
+
}
|
|
86
|
+
key={s.id}
|
|
87
|
+
text={s.title}
|
|
88
|
+
radius={2}
|
|
89
|
+
onClick={() => changeState(documentId, s)}
|
|
90
|
+
/>
|
|
91
|
+
))}
|
|
92
|
+
</Inline>
|
|
93
|
+
</Card>
|
|
94
|
+
|
|
95
|
+
{children}
|
|
96
|
+
</Stack>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {Button, Popover, useToast} from '@sanity/ui'
|
|
3
|
+
import {AddIcon} from '@sanity/icons'
|
|
4
|
+
import {UserSelectMenu} from 'sanity-plugin-utils'
|
|
5
|
+
import {useClient} from 'sanity'
|
|
6
|
+
|
|
7
|
+
import AvatarGroup from './DocumentCard/AvatarGroup'
|
|
8
|
+
import {User} from '../types'
|
|
9
|
+
|
|
10
|
+
type UserAssignmentProps = {
|
|
11
|
+
userList: User[]
|
|
12
|
+
assignees: string[]
|
|
13
|
+
documentId: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function UserAssignment(props: UserAssignmentProps) {
|
|
17
|
+
const {assignees, userList, documentId} = props
|
|
18
|
+
const client = useClient()
|
|
19
|
+
const toast = useToast()
|
|
20
|
+
const [openId, setOpenId] = React.useState<string>(``)
|
|
21
|
+
|
|
22
|
+
const addAssignee = React.useCallback(
|
|
23
|
+
(userId: string) => {
|
|
24
|
+
if (!userId) {
|
|
25
|
+
return toast.push({
|
|
26
|
+
status: 'error',
|
|
27
|
+
title: 'No user selected',
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
client
|
|
32
|
+
.patch(`workflow-metadata.${documentId}`)
|
|
33
|
+
.setIfMissing({assignees: []})
|
|
34
|
+
.insert(`after`, `assignees[-1]`, [userId])
|
|
35
|
+
.commit()
|
|
36
|
+
.then(() => {
|
|
37
|
+
return toast.push({
|
|
38
|
+
title: `Assigned user to document`,
|
|
39
|
+
description: userId,
|
|
40
|
+
status: 'success',
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
.catch((err) => {
|
|
44
|
+
console.error(err)
|
|
45
|
+
|
|
46
|
+
return toast.push({
|
|
47
|
+
title: `Failed to add assignee`,
|
|
48
|
+
description: userId,
|
|
49
|
+
status: 'error',
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
},
|
|
53
|
+
[documentId, client, toast]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const removeAssignee = React.useCallback(
|
|
57
|
+
(id: string, userId: string) => {
|
|
58
|
+
client
|
|
59
|
+
.patch(`workflow-metadata.${id}`)
|
|
60
|
+
.unset([`assignees[@ == "${userId}"]`])
|
|
61
|
+
.commit()
|
|
62
|
+
.then((res) => res)
|
|
63
|
+
.catch((err) => {
|
|
64
|
+
console.error(err)
|
|
65
|
+
|
|
66
|
+
return toast.push({
|
|
67
|
+
title: `Failed to remove assignee`,
|
|
68
|
+
description: id,
|
|
69
|
+
status: 'error',
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
},
|
|
73
|
+
[client, toast]
|
|
74
|
+
)
|
|
75
|
+
|
|
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)
|
|
85
|
+
|
|
86
|
+
return toast.push({
|
|
87
|
+
title: `Failed to clear assignees`,
|
|
88
|
+
description: id,
|
|
89
|
+
status: 'error',
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
},
|
|
93
|
+
[client, toast]
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
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>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {Card} from '@sanity/ui'
|
|
2
|
+
import React, {useCallback} from 'react'
|
|
3
|
+
import type {ArrayOfPrimitivesInputProps} from 'sanity'
|
|
4
|
+
import {setIfMissing, insert, unset} from 'sanity'
|
|
5
|
+
import {UserSelectMenu, useProjectUsers} from 'sanity-plugin-utils'
|
|
6
|
+
|
|
7
|
+
export default function UserSelectInput(props: ArrayOfPrimitivesInputProps) {
|
|
8
|
+
const {value = [], onChange} = props
|
|
9
|
+
const userList = useProjectUsers()
|
|
10
|
+
|
|
11
|
+
const onAssigneeAdd = useCallback(
|
|
12
|
+
(userId: string) => {
|
|
13
|
+
onChange([setIfMissing([]), insert([userId], `after`, [-1])])
|
|
14
|
+
},
|
|
15
|
+
[onChange]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
const onAssigneeRemove = useCallback(
|
|
19
|
+
(userId: string) => {
|
|
20
|
+
const userIdIndex = value.findIndex((v) => v === userId)
|
|
21
|
+
|
|
22
|
+
onChange(unset([userIdIndex]))
|
|
23
|
+
},
|
|
24
|
+
[onChange, value]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const onAssigneesClear = useCallback(() => {
|
|
28
|
+
onChange(unset())
|
|
29
|
+
}, [onChange])
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Card border radius={3} padding={1}>
|
|
33
|
+
<UserSelectMenu
|
|
34
|
+
open
|
|
35
|
+
value={value as string[]}
|
|
36
|
+
userList={userList}
|
|
37
|
+
onAdd={onAssigneeAdd}
|
|
38
|
+
onClear={onAssigneesClear}
|
|
39
|
+
onRemove={onAssigneeRemove}
|
|
40
|
+
/>
|
|
41
|
+
</Card>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
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
|
+
import {
|
|
17
|
+
DragDropContext,
|
|
18
|
+
Droppable,
|
|
19
|
+
Draggable,
|
|
20
|
+
DropResult,
|
|
21
|
+
} from 'react-beautiful-dnd'
|
|
22
|
+
|
|
23
|
+
import {SanityDocumentWithMetadata, State} from '../types'
|
|
24
|
+
import {DocumentCard} from './DocumentCard'
|
|
25
|
+
import Mutate from './Mutate'
|
|
26
|
+
import {useWorkflowDocuments} from '../hooks/useWorkflowDocuments'
|
|
27
|
+
|
|
28
|
+
function filterItemsByState(
|
|
29
|
+
items: SanityDocumentWithMetadata[],
|
|
30
|
+
stateId: string
|
|
31
|
+
) {
|
|
32
|
+
return items.filter((item) => item?._metadata?.state === stateId)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type WorkflowToolOptions = {
|
|
36
|
+
schemaTypes: string[]
|
|
37
|
+
states: State[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type WorkflowToolProps = {
|
|
41
|
+
tool: Tool<WorkflowToolOptions>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type MutateProps = {
|
|
45
|
+
_id: string
|
|
46
|
+
_type: string
|
|
47
|
+
state: State
|
|
48
|
+
documentId: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function WorkflowTool(props: WorkflowToolProps) {
|
|
52
|
+
const {schemaTypes = [], states = []} = props?.tool?.options ?? {}
|
|
53
|
+
|
|
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
|
+
const isDarkMode = useTheme().sanity.color.dark
|
|
63
|
+
const defaultCardTone = isDarkMode ? 'default' : 'transparent'
|
|
64
|
+
|
|
65
|
+
const userList = useProjectUsers() || []
|
|
66
|
+
const {workflowData, operations} = useWorkflowDocuments(schemaTypes)
|
|
67
|
+
|
|
68
|
+
// Data to display in cards
|
|
69
|
+
const {data, loading, error} = workflowData
|
|
70
|
+
|
|
71
|
+
// Operations to perform on cards
|
|
72
|
+
const {move} = operations
|
|
73
|
+
|
|
74
|
+
const documentsWithoutMetadataIds = data
|
|
75
|
+
.filter((doc) => !doc._metadata)
|
|
76
|
+
.map((d) => d._id.replace(`drafts.`, ``))
|
|
77
|
+
|
|
78
|
+
const importDocuments = React.useCallback(async (ids: string[]) => {
|
|
79
|
+
toast.push({
|
|
80
|
+
title: 'Importing documents',
|
|
81
|
+
status: 'info',
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const tx = ids.reduce((item, documentId) => {
|
|
85
|
+
return item.createOrReplace({
|
|
86
|
+
_id: `workflow-metadata.${documentId}`,
|
|
87
|
+
_type: 'workflow.metadata',
|
|
88
|
+
state: states[0].id,
|
|
89
|
+
documentId,
|
|
90
|
+
})
|
|
91
|
+
}, client.transaction())
|
|
92
|
+
|
|
93
|
+
await tx.commit()
|
|
94
|
+
|
|
95
|
+
toast.push({
|
|
96
|
+
title: 'Imported documents',
|
|
97
|
+
status: 'success',
|
|
98
|
+
})
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
const handleDragEnd = React.useCallback(
|
|
102
|
+
(result: DropResult) => {
|
|
103
|
+
const {draggableId, source, destination} = result
|
|
104
|
+
console.log(
|
|
105
|
+
`sending ${draggableId} from ${source.droppableId} to ${destination?.droppableId}`
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if (!destination || destination.droppableId === source.droppableId) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The list of mutating docs is how we un/publish documents
|
|
113
|
+
const mutatingDoc = move(draggableId, destination, states)
|
|
114
|
+
|
|
115
|
+
if (mutatingDoc) {
|
|
116
|
+
// @ts-ignore
|
|
117
|
+
// @todo not sure if these types should be updated. will documentId every be undefined here?
|
|
118
|
+
setMutatingDocs((current) => [...current, mutatingDoc])
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[move, states]
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if (!states?.length) {
|
|
125
|
+
return (
|
|
126
|
+
<Container width={1} padding={5}>
|
|
127
|
+
<Feedback
|
|
128
|
+
tone="caution"
|
|
129
|
+
title="Plugin options error"
|
|
130
|
+
description="No States defined in plugin config"
|
|
131
|
+
/>
|
|
132
|
+
</Container>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (error) {
|
|
137
|
+
return (
|
|
138
|
+
<Container width={1} padding={5}>
|
|
139
|
+
<Feedback tone="critical" title="Error with query" />
|
|
140
|
+
</Container>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<>
|
|
146
|
+
{mutatingDocs.length ? (
|
|
147
|
+
<div style={{position: `absolute`, bottom: 0, background: 'red'}}>
|
|
148
|
+
{mutatingDocs.map((mutate) => (
|
|
149
|
+
<Mutate
|
|
150
|
+
key={mutate._id}
|
|
151
|
+
{...mutate}
|
|
152
|
+
onComplete={mutationFinished}
|
|
153
|
+
/>
|
|
154
|
+
))}
|
|
155
|
+
</div>
|
|
156
|
+
) : null}
|
|
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}>
|
|
175
|
+
<Grid columns={states.length} height="fill">
|
|
176
|
+
{states.map((state: State, stateIndex: number) => (
|
|
177
|
+
<Card key={state.id} borderLeft={stateIndex > 0}>
|
|
178
|
+
<Card paddingY={4} padding={3} style={{pointerEvents: `none`}}>
|
|
179
|
+
<Label>{state.title}</Label>
|
|
180
|
+
</Card>
|
|
181
|
+
<Droppable droppableId={state.id}>
|
|
182
|
+
{(provided, snapshot) => (
|
|
183
|
+
<Card
|
|
184
|
+
ref={provided.innerRef}
|
|
185
|
+
tone={snapshot.isDraggingOver ? `primary` : defaultCardTone}
|
|
186
|
+
height="fill"
|
|
187
|
+
>
|
|
188
|
+
{loading ? (
|
|
189
|
+
<Flex padding={5} align="center" justify="center">
|
|
190
|
+
<Spinner muted />
|
|
191
|
+
</Flex>
|
|
192
|
+
) : null}
|
|
193
|
+
|
|
194
|
+
{data.length > 0 &&
|
|
195
|
+
filterItemsByState(data, state.id).map(
|
|
196
|
+
(item, itemIndex) => (
|
|
197
|
+
// The metadata's documentId is always the published one
|
|
198
|
+
<Draggable
|
|
199
|
+
key={item?._metadata?.documentId as string}
|
|
200
|
+
draggableId={item?._metadata?.documentId as string}
|
|
201
|
+
index={itemIndex}
|
|
202
|
+
>
|
|
203
|
+
{(draggableProvided, draggableSnapshot) => (
|
|
204
|
+
<div
|
|
205
|
+
ref={draggableProvided.innerRef}
|
|
206
|
+
{...draggableProvided.draggableProps}
|
|
207
|
+
{...draggableProvided.dragHandleProps}
|
|
208
|
+
>
|
|
209
|
+
<DocumentCard
|
|
210
|
+
isDragging={draggableSnapshot.isDragging}
|
|
211
|
+
item={item}
|
|
212
|
+
userList={userList}
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</Draggable>
|
|
217
|
+
)
|
|
218
|
+
)}
|
|
219
|
+
</Card>
|
|
220
|
+
)}
|
|
221
|
+
</Droppable>
|
|
222
|
+
</Card>
|
|
223
|
+
))}
|
|
224
|
+
</Grid>
|
|
225
|
+
</DragDropContext>
|
|
226
|
+
</>
|
|
227
|
+
)
|
|
228
|
+
}
|