sanity-plugin-workflow 1.0.0-beta.1 → 1.0.0-beta.4
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 +73 -12
- package/lib/{src/index.d.ts → index.d.ts} +3 -3
- package/lib/index.esm.js +1800 -1
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +1813 -1
- package/lib/index.js.map +1 -1
- package/package.json +51 -40
- package/src/actions/AssignWorkflow.tsx +48 -0
- package/src/actions/BeginWorkflow.tsx +68 -0
- package/src/actions/CompleteWorkflow.tsx +41 -0
- package/src/actions/RequestReviewAction.js +1 -7
- package/src/actions/UpdateWorkflow.tsx +142 -0
- package/src/badges/AssigneesBadge.tsx +52 -0
- package/src/badges/{index.tsx → StateBadge.tsx} +4 -8
- package/src/components/DocumentCard/AvatarGroup.tsx +12 -8
- package/src/components/DocumentCard/CompleteButton.tsx +53 -0
- package/src/components/DocumentCard/EditButton.tsx +3 -2
- package/src/components/DocumentCard/Field.tsx +38 -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 +156 -50
- package/src/components/DocumentList.tsx +122 -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 +73 -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/Validators.tsx +229 -0
- package/src/components/WorkflowTool.tsx +302 -163
- package/src/constants/index.ts +31 -0
- package/src/helpers/arraysContainMatchingString.ts +6 -0
- package/src/helpers/filterItemsAndSort.ts +41 -0
- package/src/helpers/initialRank.ts +13 -0
- package/src/hooks/useWorkflowDocuments.tsx +62 -70
- package/src/hooks/useWorkflowMetadata.tsx +0 -1
- package/src/index.ts +38 -58
- 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/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,68 @@
|
|
|
1
|
+
import {useCallback, useState} from 'react'
|
|
2
|
+
import {SplitVerticalIcon} from '@sanity/icons'
|
|
3
|
+
import {DocumentActionProps, useClient} from 'sanity'
|
|
4
|
+
import {useToast} from '@sanity/ui'
|
|
5
|
+
import {LexoRank} from 'lexorank'
|
|
6
|
+
|
|
7
|
+
import {API_VERSION} from '../constants'
|
|
8
|
+
import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
|
|
9
|
+
import {State} from '../types'
|
|
10
|
+
|
|
11
|
+
export function BeginWorkflow(props: DocumentActionProps, states: State[]) {
|
|
12
|
+
const {id, draft} = props
|
|
13
|
+
const {data, loading, error} = useWorkflowMetadata(id, states)
|
|
14
|
+
const client = useClient({apiVersion: API_VERSION})
|
|
15
|
+
const toast = useToast()
|
|
16
|
+
const [beginning, setBeginning] = useState(false)
|
|
17
|
+
const [complete, setComplete] = useState(false)
|
|
18
|
+
|
|
19
|
+
if (error) {
|
|
20
|
+
console.error(error)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const handle = useCallback(async () => {
|
|
24
|
+
setBeginning(true)
|
|
25
|
+
const lowestOrderFirstState = await client.fetch(
|
|
26
|
+
`*[_type == "workflow.metadata" && state == $state]|order(orderRank)[0].orderRank`,
|
|
27
|
+
{state: states[0].id}
|
|
28
|
+
)
|
|
29
|
+
client
|
|
30
|
+
.createIfNotExists(
|
|
31
|
+
{
|
|
32
|
+
_id: `workflow-metadata.${id}`,
|
|
33
|
+
_type: `workflow.metadata`,
|
|
34
|
+
documentId: id,
|
|
35
|
+
state: states[0].id,
|
|
36
|
+
orderRank: lowestOrderFirstState
|
|
37
|
+
? LexoRank.parse(lowestOrderFirstState).genNext().toString()
|
|
38
|
+
: LexoRank.min().toString(),
|
|
39
|
+
},
|
|
40
|
+
// Faster!
|
|
41
|
+
{visibility: 'async'}
|
|
42
|
+
)
|
|
43
|
+
.then(() => {
|
|
44
|
+
toast.push({
|
|
45
|
+
status: 'success',
|
|
46
|
+
title: 'Workflow started',
|
|
47
|
+
description: `Document is now "${states[0].title}"`,
|
|
48
|
+
})
|
|
49
|
+
setBeginning(false)
|
|
50
|
+
// Optimistically remove action
|
|
51
|
+
setComplete(true)
|
|
52
|
+
})
|
|
53
|
+
}, [id, states, client, toast])
|
|
54
|
+
|
|
55
|
+
if (!draft || complete || data.metadata) {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
icon: SplitVerticalIcon,
|
|
61
|
+
type: 'dialog',
|
|
62
|
+
disabled: data?.metadata || loading || error || beginning || complete,
|
|
63
|
+
label: beginning ? `Beginning...` : `Begin Workflow`,
|
|
64
|
+
onHandle: () => {
|
|
65
|
+
handle()
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import {useCallback} from 'react'
|
|
2
|
+
import {CheckmarkIcon} from '@sanity/icons'
|
|
3
|
+
import {DocumentActionProps, useClient} from 'sanity'
|
|
4
|
+
|
|
5
|
+
import {API_VERSION} from '../constants'
|
|
6
|
+
import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
|
|
7
|
+
import {State} from '../types'
|
|
8
|
+
|
|
9
|
+
export function CompleteWorkflow(props: DocumentActionProps, states: State[]) {
|
|
10
|
+
const {id} = props
|
|
11
|
+
const {data, loading, error} = useWorkflowMetadata(id, states)
|
|
12
|
+
const client = useClient({apiVersion: API_VERSION})
|
|
13
|
+
|
|
14
|
+
if (error) {
|
|
15
|
+
console.error(error)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const handle = useCallback(() => {
|
|
19
|
+
client.delete(`workflow-metadata.${id}`)
|
|
20
|
+
}, [id, client])
|
|
21
|
+
|
|
22
|
+
const isLastState = data?.state?.id === states[states.length - 1].id
|
|
23
|
+
|
|
24
|
+
if (!data.metadata) {
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
icon: CheckmarkIcon,
|
|
30
|
+
type: 'dialog',
|
|
31
|
+
disabled: loading || error || !isLastState,
|
|
32
|
+
label: `Complete Workflow`,
|
|
33
|
+
title: isLastState
|
|
34
|
+
? `Removes the document from the Workflow process`
|
|
35
|
+
: `Cannot remove from workflow until in the last state`,
|
|
36
|
+
onHandle: () => {
|
|
37
|
+
handle()
|
|
38
|
+
},
|
|
39
|
+
color: 'positive',
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -38,13 +38,7 @@ export function RequestReviewAction(props) {
|
|
|
38
38
|
return {
|
|
39
39
|
dialog: showWizardDialog && {
|
|
40
40
|
type: 'popover',
|
|
41
|
-
content:
|
|
42
|
-
<RequestReviewWizard
|
|
43
|
-
metadata={metadata.data}
|
|
44
|
-
onClose={onClose}
|
|
45
|
-
onSend={onSend}
|
|
46
|
-
/>
|
|
47
|
-
),
|
|
41
|
+
content: <RequestReviewWizard metadata={metadata.data} onClose={onClose} onSend={onSend} />,
|
|
48
42
|
onClose: props.onComplete,
|
|
49
43
|
},
|
|
50
44
|
disabled: showWizardDialog,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// import {useState} from 'react'
|
|
2
|
+
import {ArrowRightIcon, ArrowLeftIcon} from '@sanity/icons'
|
|
3
|
+
import {useToast} from '@sanity/ui'
|
|
4
|
+
import {useCurrentUser, useValidationStatus} from 'sanity'
|
|
5
|
+
import {DocumentActionProps, useClient} from 'sanity'
|
|
6
|
+
|
|
7
|
+
import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
|
|
8
|
+
import {API_VERSION} from '../constants'
|
|
9
|
+
import {State} from '../types'
|
|
10
|
+
import {arraysContainMatchingString} from '../helpers/arraysContainMatchingString'
|
|
11
|
+
|
|
12
|
+
export function UpdateWorkflow(
|
|
13
|
+
props: DocumentActionProps,
|
|
14
|
+
allStates: State[],
|
|
15
|
+
actionState: State
|
|
16
|
+
) {
|
|
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
|
+
|
|
24
|
+
const user = useCurrentUser()
|
|
25
|
+
const client = useClient({apiVersion: API_VERSION})
|
|
26
|
+
const toast = useToast()
|
|
27
|
+
const currentUser = useCurrentUser()
|
|
28
|
+
|
|
29
|
+
const {data, loading, error} = useWorkflowMetadata(id, allStates)
|
|
30
|
+
const {state: currentState} = data
|
|
31
|
+
const {assignees = []} = data?.metadata ?? {}
|
|
32
|
+
|
|
33
|
+
if (error) {
|
|
34
|
+
console.error(error)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const onHandle = (documentId: string, newState: State) => {
|
|
38
|
+
client
|
|
39
|
+
.patch(`workflow-metadata.${documentId}`)
|
|
40
|
+
.set({state: newState.id})
|
|
41
|
+
.commit()
|
|
42
|
+
.then(() => {
|
|
43
|
+
props.onComplete()
|
|
44
|
+
toast.push({
|
|
45
|
+
status: 'success',
|
|
46
|
+
title: `Document state now "${newState.title}"`,
|
|
47
|
+
})
|
|
48
|
+
// Perform document operations after State changes
|
|
49
|
+
// If State has changed and the document needs to be un/published
|
|
50
|
+
// This functionality was deemed too dangerous / unexpected
|
|
51
|
+
// Revisit with improved UX
|
|
52
|
+
// if (!ops.publish.disabled && nextOperation === 'publish') {
|
|
53
|
+
// ops.publish.execute()
|
|
54
|
+
// } else if (!ops.unpublish.disabled && nextOperation === 'unpublish') {
|
|
55
|
+
// ops.unpublish.execute()
|
|
56
|
+
// }
|
|
57
|
+
})
|
|
58
|
+
.catch((err) => {
|
|
59
|
+
props.onComplete()
|
|
60
|
+
console.error(err)
|
|
61
|
+
toast.push({
|
|
62
|
+
status: 'error',
|
|
63
|
+
title: `Document state update failed`,
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Remove button if:
|
|
69
|
+
// Document is not in Workflow OR
|
|
70
|
+
// The current State is the same as this actions State
|
|
71
|
+
if (!data.metadata || (currentState && currentState.id === actionState.id)) {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const currentStateIndex = allStates.findIndex(
|
|
76
|
+
(s) => s.id === currentState?.id
|
|
77
|
+
)
|
|
78
|
+
const actionStateIndex = allStates.findIndex((s) => s.id === actionState.id)
|
|
79
|
+
const direction = actionStateIndex > currentStateIndex ? 'promote' : 'demote'
|
|
80
|
+
const DirectionIcon = direction === 'promote' ? ArrowRightIcon : ArrowLeftIcon
|
|
81
|
+
const directionLabel = direction === 'promote' ? 'Promote' : 'Demote'
|
|
82
|
+
|
|
83
|
+
let title = `${directionLabel} State to "${actionState.title}"`
|
|
84
|
+
|
|
85
|
+
const userRoleCanUpdateState =
|
|
86
|
+
user?.roles?.length && actionState?.roles?.length
|
|
87
|
+
? // If the Action state is limited to specific roles
|
|
88
|
+
// check that the current user has one of those roles
|
|
89
|
+
arraysContainMatchingString(
|
|
90
|
+
user.roles.map((r) => r.name),
|
|
91
|
+
actionState.roles
|
|
92
|
+
)
|
|
93
|
+
: // No roles specified on the next state, so anyone can update
|
|
94
|
+
actionState?.roles?.length !== 0
|
|
95
|
+
|
|
96
|
+
if (!userRoleCanUpdateState) {
|
|
97
|
+
title = `Your User role cannot ${directionLabel} State to "${actionState.title}"`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const actionStateIsAValidTransition =
|
|
101
|
+
currentState?.id && currentState.transitions.length
|
|
102
|
+
? // If the Current State limits transitions to specific States
|
|
103
|
+
// Check that the Action State is in Current State's transitions array
|
|
104
|
+
currentState.transitions.includes(actionState.id)
|
|
105
|
+
: // Otherwise this isn't a problem
|
|
106
|
+
true
|
|
107
|
+
|
|
108
|
+
if (!actionStateIsAValidTransition) {
|
|
109
|
+
title = `You cannot ${directionLabel} State to "${actionState.title}" from "${currentState?.title}"`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const userAssignmentCanUpdateState = actionState.requireAssignment
|
|
113
|
+
? // If the Action State requires assigned users
|
|
114
|
+
// Check the current user ID is in the assignees array
|
|
115
|
+
currentUser && assignees.length && assignees.includes(currentUser.id)
|
|
116
|
+
: // Otherwise this isn't a problem
|
|
117
|
+
true
|
|
118
|
+
|
|
119
|
+
if (!userAssignmentCanUpdateState) {
|
|
120
|
+
title = `You must be assigned to the document to ${directionLabel} State to "${actionState.title}"`
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (hasValidationErrors) {
|
|
124
|
+
title = `Document has validation errors, cannot ${directionLabel} State to "${actionState.title}"`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
icon: DirectionIcon,
|
|
129
|
+
disabled:
|
|
130
|
+
loading ||
|
|
131
|
+
error ||
|
|
132
|
+
isValidating ||
|
|
133
|
+
hasValidationErrors ||
|
|
134
|
+
!currentState ||
|
|
135
|
+
!userRoleCanUpdateState ||
|
|
136
|
+
!actionStateIsAValidTransition ||
|
|
137
|
+
!userAssignmentCanUpdateState,
|
|
138
|
+
title,
|
|
139
|
+
label: actionState.title,
|
|
140
|
+
onHandle: () => onHandle(id, actionState),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {CurrentUser, DocumentBadgeDescription} from 'sanity'
|
|
2
|
+
import {useProjectUsers} from 'sanity-plugin-utils'
|
|
3
|
+
import {API_VERSION} from '../constants'
|
|
4
|
+
import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
|
|
5
|
+
|
|
6
|
+
import {State} from '../types'
|
|
7
|
+
|
|
8
|
+
export function AssigneesBadge(
|
|
9
|
+
states: State[],
|
|
10
|
+
documentId: string,
|
|
11
|
+
currentUser: CurrentUser | null
|
|
12
|
+
): DocumentBadgeDescription | null {
|
|
13
|
+
const {data, loading, error} = useWorkflowMetadata(documentId, states)
|
|
14
|
+
const {metadata} = data
|
|
15
|
+
const userList = useProjectUsers({apiVersion: API_VERSION})
|
|
16
|
+
|
|
17
|
+
if (loading || error || !metadata) {
|
|
18
|
+
if (error) {
|
|
19
|
+
console.error(error)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!metadata?.assignees?.length) {
|
|
26
|
+
return {
|
|
27
|
+
label: 'Unassigned',
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const {assignees} = metadata ?? []
|
|
32
|
+
const hasMe = currentUser ? assignees.some((assignee) => assignee === currentUser.id) : false
|
|
33
|
+
const assigneesCount = hasMe ? assignees.length - 1 : assignees.length
|
|
34
|
+
const assigneeUsers = userList.filter((user) => assignees.includes(user.id))
|
|
35
|
+
const title = assigneeUsers.map((user) => user.displayName).join(', ')
|
|
36
|
+
|
|
37
|
+
let label
|
|
38
|
+
|
|
39
|
+
if (hasMe && assigneesCount === 0) {
|
|
40
|
+
label = 'Assigned to Me'
|
|
41
|
+
} else if (hasMe && assigneesCount > 0) {
|
|
42
|
+
label = `Me and ${assigneesCount} ${assigneesCount === 1 ? 'other' : 'others'}`
|
|
43
|
+
} else {
|
|
44
|
+
label = `${assigneesCount} assigned`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
label,
|
|
49
|
+
title,
|
|
50
|
+
color: 'primary',
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import {DocumentBadgeDescription
|
|
1
|
+
import {DocumentBadgeDescription} from 'sanity'
|
|
2
2
|
import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
|
|
3
3
|
|
|
4
4
|
import {State} from '../types'
|
|
5
5
|
|
|
6
|
-
export function StateBadge(
|
|
7
|
-
|
|
8
|
-
states: State[]
|
|
9
|
-
): DocumentBadgeDescription | null {
|
|
10
|
-
const {id} = props
|
|
11
|
-
const {data, loading, error} = useWorkflowMetadata(id, states)
|
|
6
|
+
export function StateBadge(states: State[], documentId: string): DocumentBadgeDescription | null {
|
|
7
|
+
const {data, loading, error} = useWorkflowMetadata(documentId, states)
|
|
12
8
|
const {state} = data
|
|
13
9
|
|
|
14
10
|
if (loading || error) {
|
|
@@ -25,7 +21,7 @@ export function StateBadge(
|
|
|
25
21
|
|
|
26
22
|
return {
|
|
27
23
|
label: state.title,
|
|
28
|
-
title: state.title,
|
|
24
|
+
// title: state.title,
|
|
29
25
|
color: state?.color,
|
|
30
26
|
}
|
|
31
27
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import {Box, Flex, Text} from '@sanity/ui'
|
|
3
|
-
import {UserAvatar} from 'sanity'
|
|
3
|
+
import {useCurrentUser, UserAvatar} from 'sanity'
|
|
4
4
|
|
|
5
5
|
import {User} from '../../types'
|
|
6
6
|
|
|
@@ -10,22 +10,26 @@ type AvatarGroupProps = {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export default function AvatarGroup(props: AvatarGroupProps) {
|
|
13
|
-
const
|
|
13
|
+
const currentUser = useCurrentUser()
|
|
14
|
+
const {users, max = 4} = props
|
|
14
15
|
|
|
15
16
|
const len = users?.length
|
|
16
|
-
const visibleUsers = React.useMemo(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
const {me, visibleUsers} = React.useMemo(() => {
|
|
18
|
+
return {
|
|
19
|
+
me: currentUser?.id ? users.find((u) => u.id === currentUser.id) : undefined,
|
|
20
|
+
visibleUsers: users.filter((u) => u.id !== currentUser?.id).slice(0, max - 1),
|
|
21
|
+
}
|
|
22
|
+
}, [users, max, currentUser])
|
|
20
23
|
|
|
21
24
|
if (!users?.length) {
|
|
22
25
|
return null
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
return (
|
|
26
|
-
<Flex align="center">
|
|
29
|
+
<Flex align="center" gap={1}>
|
|
30
|
+
{me ? <UserAvatar user={me} /> : null}
|
|
27
31
|
{visibleUsers.map((user) => (
|
|
28
|
-
<Box key={user.id} style={{marginRight: -
|
|
32
|
+
<Box key={user.id} style={{marginRight: -8}}>
|
|
29
33
|
<UserAvatar user={user} />
|
|
30
34
|
</Box>
|
|
31
35
|
))}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {Button, useToast} from '@sanity/ui'
|
|
3
|
+
import {CheckmarkIcon} from '@sanity/icons'
|
|
4
|
+
import {useClient} from 'sanity'
|
|
5
|
+
|
|
6
|
+
import {API_VERSION} from '../../constants'
|
|
7
|
+
|
|
8
|
+
type CompleteButtonProps = {
|
|
9
|
+
documentId: string
|
|
10
|
+
disabled: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function CompleteButton(props: CompleteButtonProps) {
|
|
14
|
+
const {documentId, disabled = false} = props
|
|
15
|
+
const client = useClient({apiVersion: API_VERSION})
|
|
16
|
+
const toast = useToast()
|
|
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,
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
.catch(() => {
|
|
30
|
+
toast.push({
|
|
31
|
+
status: 'error',
|
|
32
|
+
title: 'Could not complete Workflow',
|
|
33
|
+
description: id,
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
},
|
|
37
|
+
[client, toast]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
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
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
1
|
import {Button} from '@sanity/ui'
|
|
3
2
|
import {EditIcon} from '@sanity/icons'
|
|
4
3
|
import {useRouter} from 'sanity/router'
|
|
@@ -6,10 +5,11 @@ import {useRouter} from 'sanity/router'
|
|
|
6
5
|
type EditButtonProps = {
|
|
7
6
|
id: string
|
|
8
7
|
type: string
|
|
8
|
+
disabled?: boolean
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export default function EditButton(props: EditButtonProps) {
|
|
12
|
-
const {id, type} = props
|
|
12
|
+
const {id, type, disabled = false} = props
|
|
13
13
|
const {navigateIntent} = useRouter()
|
|
14
14
|
|
|
15
15
|
return (
|
|
@@ -22,6 +22,7 @@ export default function EditButton(props: EditButtonProps) {
|
|
|
22
22
|
tabIndex={-1}
|
|
23
23
|
icon={EditIcon}
|
|
24
24
|
text="Edit"
|
|
25
|
+
disabled={disabled}
|
|
25
26
|
/>
|
|
26
27
|
)
|
|
27
28
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {Flex, Card, Spinner} from '@sanity/ui'
|
|
2
|
+
import {Preview, SanityDocument, StringInputProps, useSchema} from 'sanity'
|
|
3
|
+
import {useListeningQuery, Feedback} from 'sanity-plugin-utils'
|
|
4
|
+
|
|
5
|
+
import EditButton from './EditButton'
|
|
6
|
+
|
|
7
|
+
// TODO: Update this to use the same component as the Tool
|
|
8
|
+
export default function Field(props: StringInputProps) {
|
|
9
|
+
const schema = useSchema()
|
|
10
|
+
const {data, loading, error} = useListeningQuery<SanityDocument>(
|
|
11
|
+
`*[_id in [$id, $draftId]]|order(_updatedAt)[0]`,
|
|
12
|
+
{
|
|
13
|
+
params: {
|
|
14
|
+
id: String(props.value),
|
|
15
|
+
draftId: `drafts.${String(props.value)}`,
|
|
16
|
+
},
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if (loading) {
|
|
21
|
+
return <Spinner />
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const schemaType = schema.get(data?._type ?? ``)
|
|
25
|
+
|
|
26
|
+
if (error || !data?._type || !schemaType) {
|
|
27
|
+
return <Feedback tone="critical" title="Error with query" />
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Card border padding={2}>
|
|
32
|
+
<Flex align="center" justify="space-between" gap={2}>
|
|
33
|
+
<Preview layout="default" value={data} schemaType={schemaType} />
|
|
34
|
+
<EditButton id={data._id} type={data._type} />
|
|
35
|
+
</Flex>
|
|
36
|
+
</Card>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import {ErrorOutlineIcon, WarningOutlineIcon} from '@sanity/icons'
|
|
2
|
+
import {ValidationMarker} from '@sanity/types'
|
|
3
|
+
import {Box, Text, Tooltip} from '@sanity/ui'
|
|
4
|
+
import {TextWithTone} from 'sanity'
|
|
5
|
+
|
|
6
|
+
type ValidationStatusProps = {
|
|
7
|
+
validation: ValidationMarker[]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ValidationStatus(props: ValidationStatusProps) {
|
|
11
|
+
const {validation = []} = props
|
|
12
|
+
|
|
13
|
+
if (!validation.length) {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const hasError = validation.some((item) => item.level === 'error')
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Tooltip
|
|
21
|
+
portal
|
|
22
|
+
content={
|
|
23
|
+
<Box padding={2}>
|
|
24
|
+
<Text size={1}>
|
|
25
|
+
{validation.length === 1
|
|
26
|
+
? `1 validation issue`
|
|
27
|
+
: `${validation.length} validation issues`}
|
|
28
|
+
</Text>
|
|
29
|
+
</Box>
|
|
30
|
+
}
|
|
31
|
+
>
|
|
32
|
+
<TextWithTone tone={hasError ? `critical` : `caution`} size={1}>
|
|
33
|
+
{hasError ? <ErrorOutlineIcon /> : <WarningOutlineIcon />}
|
|
34
|
+
</TextWithTone>
|
|
35
|
+
</Tooltip>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {EditIcon} from '@sanity/icons'
|
|
2
|
+
import {PreviewValue, SanityDocument} from '@sanity/types'
|
|
3
|
+
import {Box, Text, Tooltip} from '@sanity/ui'
|
|
4
|
+
import {TextWithTone} from 'sanity'
|
|
5
|
+
|
|
6
|
+
import {TimeAgo} from './TimeAgo'
|
|
7
|
+
|
|
8
|
+
export function DraftStatus(props: {document?: PreviewValue | Partial<SanityDocument> | null}) {
|
|
9
|
+
const {document} = props
|
|
10
|
+
const updatedAt = document && '_updatedAt' in document && document._updatedAt
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<Tooltip
|
|
14
|
+
portal
|
|
15
|
+
content={
|
|
16
|
+
<Box padding={2}>
|
|
17
|
+
<Text size={1}>
|
|
18
|
+
{document ? (
|
|
19
|
+
<>Edited {updatedAt && <TimeAgo time={updatedAt} />}</>
|
|
20
|
+
) : (
|
|
21
|
+
<>No unpublished edits</>
|
|
22
|
+
)}
|
|
23
|
+
</Text>
|
|
24
|
+
</Box>
|
|
25
|
+
}
|
|
26
|
+
>
|
|
27
|
+
<TextWithTone tone="caution" dimmed={!document} muted={!document} size={1}>
|
|
28
|
+
<EditIcon />
|
|
29
|
+
</TextWithTone>
|
|
30
|
+
</Tooltip>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {PublishIcon} from '@sanity/icons'
|
|
2
|
+
import {PreviewValue, SanityDocument} from '@sanity/types'
|
|
3
|
+
import {Box, Text, Tooltip} from '@sanity/ui'
|
|
4
|
+
import {TextWithTone} from 'sanity'
|
|
5
|
+
|
|
6
|
+
import {TimeAgo} from './TimeAgo'
|
|
7
|
+
|
|
8
|
+
export function PublishedStatus(props: {
|
|
9
|
+
document?: PreviewValue | Partial<SanityDocument> | null
|
|
10
|
+
}) {
|
|
11
|
+
const {document} = props
|
|
12
|
+
const updatedAt = document && '_updatedAt' in document && document._updatedAt
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Tooltip
|
|
16
|
+
portal
|
|
17
|
+
content={
|
|
18
|
+
<Box padding={2}>
|
|
19
|
+
<Text size={1}>
|
|
20
|
+
{document ? (
|
|
21
|
+
<>Published {updatedAt && <TimeAgo time={updatedAt} />}</>
|
|
22
|
+
) : (
|
|
23
|
+
<>Not published</>
|
|
24
|
+
)}
|
|
25
|
+
</Text>
|
|
26
|
+
</Box>
|
|
27
|
+
}
|
|
28
|
+
>
|
|
29
|
+
<TextWithTone
|
|
30
|
+
tone="positive"
|
|
31
|
+
dimmed={!document}
|
|
32
|
+
muted={!document}
|
|
33
|
+
size={1}
|
|
34
|
+
>
|
|
35
|
+
<PublishIcon />
|
|
36
|
+
</TextWithTone>
|
|
37
|
+
</Tooltip>
|
|
38
|
+
)
|
|
39
|
+
}
|