sanity-plugin-workflow 1.0.0-beta.1 → 1.0.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2107 -1
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +2120 -1
- package/lib/index.js.map +1 -1
- package/package.json +51 -40
- package/src/actions/AssignWorkflow.tsx +47 -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,80 @@
|
|
|
1
|
+
import {LexoRank} from 'lexorank'
|
|
2
|
+
|
|
3
|
+
function generateMiddleValue(ranks: (LexoRank | undefined)[]) {
|
|
4
|
+
// Has no undefined values
|
|
5
|
+
if (!ranks.some((rank) => !rank)) {
|
|
6
|
+
return ranks
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Find the first undefined value
|
|
10
|
+
const firstUndefined = ranks.findIndex((rank) => !rank)
|
|
11
|
+
|
|
12
|
+
// Find the first defined value after the undefined value
|
|
13
|
+
const firstDefinedAfter = ranks.findIndex(
|
|
14
|
+
(rank, index) => rank && index > firstUndefined
|
|
15
|
+
)
|
|
16
|
+
// Find the first defined value before the undefined value
|
|
17
|
+
const firstDefinedBefore = ranks.findLastIndex(
|
|
18
|
+
(rank, index) => rank && index < firstUndefined
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if (firstDefinedAfter === -1 || firstDefinedBefore === -1) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`Unable to generate middle value between indexes ${firstDefinedBefore} and ${firstDefinedAfter}`
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const beforeRank = ranks[firstDefinedBefore]
|
|
28
|
+
const afterRank = ranks[firstDefinedAfter]
|
|
29
|
+
|
|
30
|
+
if (
|
|
31
|
+
!beforeRank ||
|
|
32
|
+
typeof beforeRank === 'undefined' ||
|
|
33
|
+
!afterRank ||
|
|
34
|
+
typeof afterRank === 'undefined'
|
|
35
|
+
) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Unable to generate middle value between indexes ${firstDefinedBefore} and ${firstDefinedAfter}`
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Generate a new value between the two
|
|
42
|
+
const between = beforeRank.between(afterRank)
|
|
43
|
+
|
|
44
|
+
// Calculate the middle index between the defined values
|
|
45
|
+
const middle = Math.floor((firstDefinedAfter + firstDefinedBefore) / 2)
|
|
46
|
+
|
|
47
|
+
if (ranks[middle]) {
|
|
48
|
+
throw new Error(`Should not have overwritten value at index ${middle}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Insert the new value into the array
|
|
52
|
+
ranks[middle] = between
|
|
53
|
+
|
|
54
|
+
// Return as a new array
|
|
55
|
+
return ranks
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Generates an array of LexoRanks between two values
|
|
59
|
+
export function generateMultipleOrderRanks(
|
|
60
|
+
count: number,
|
|
61
|
+
start?: LexoRank,
|
|
62
|
+
end?: LexoRank
|
|
63
|
+
): LexoRank[] {
|
|
64
|
+
// Begin array with correct size
|
|
65
|
+
let ranks = [...Array(count)]
|
|
66
|
+
|
|
67
|
+
// Use or create default values
|
|
68
|
+
const rankStart = start ?? LexoRank.min().genNext().genNext()
|
|
69
|
+
const rankEnd = end ?? LexoRank.max().genPrev().genPrev()
|
|
70
|
+
|
|
71
|
+
ranks[0] = rankStart
|
|
72
|
+
ranks[count - 1] = rankEnd
|
|
73
|
+
|
|
74
|
+
// Keep processing the array until every value between undefined values is defined
|
|
75
|
+
for (let i = 0; i < count; i++) {
|
|
76
|
+
ranks = generateMiddleValue(ranks)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return ranks.sort((a, b) => a.toString().localeCompare(b.toString()))
|
|
80
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {LexoRank} from 'lexorank'
|
|
2
|
+
|
|
3
|
+
// Use in initial value field by passing in the rank value of the last document
|
|
4
|
+
// If not value passed, generate a sensibly low rank
|
|
5
|
+
export default function initialRank(lastRankValue = ``): string {
|
|
6
|
+
const lastRank =
|
|
7
|
+
lastRankValue && typeof lastRankValue === 'string'
|
|
8
|
+
? LexoRank.parse(lastRankValue)
|
|
9
|
+
: LexoRank.min()
|
|
10
|
+
const nextRank = lastRank.genNext().genNext()
|
|
11
|
+
|
|
12
|
+
return (nextRank as any).value
|
|
13
|
+
}
|
|
@@ -1,88 +1,76 @@
|
|
|
1
|
+
import {DraggableLocation} from '@hello-pangea/dnd'
|
|
2
|
+
import {useToast} from '@sanity/ui'
|
|
3
|
+
import groq from 'groq'
|
|
1
4
|
import React from 'react'
|
|
5
|
+
import {useClient} from 'sanity'
|
|
2
6
|
import {useListeningQuery} from 'sanity-plugin-utils'
|
|
3
|
-
import {useToast} from '@sanity/ui'
|
|
4
|
-
import {SanityDocumentLike, useClient} from 'sanity'
|
|
5
|
-
import {DraggableLocation} from 'react-beautiful-dnd'
|
|
6
|
-
import {SanityDocumentWithMetadata, Metadata, State} from '../types'
|
|
7
|
-
|
|
8
|
-
type DocumentsAndMetadata = {
|
|
9
|
-
documents: SanityDocumentLike[]
|
|
10
|
-
metadata: Metadata[]
|
|
11
|
-
}
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
8
|
+
import {API_VERSION} from '../constants'
|
|
9
|
+
import {SanityDocumentWithMetadata, State} from '../types'
|
|
10
|
+
|
|
11
|
+
const QUERY = groq`*[_type == "workflow.metadata"]|order(orderRank){
|
|
12
|
+
"_metadata": {
|
|
13
|
+
_rev,
|
|
14
|
+
assignees,
|
|
15
|
+
documentId,
|
|
16
|
+
state,
|
|
17
|
+
orderRank
|
|
18
|
+
},
|
|
19
|
+
...(
|
|
20
|
+
*[_id in [^.documentId, "drafts." + ^.documentId]]|order(_updatedAt)[0]{
|
|
21
|
+
_id,
|
|
22
|
+
_type,
|
|
23
|
+
_rev,
|
|
24
|
+
_updatedAt
|
|
25
|
+
}
|
|
26
|
+
)
|
|
24
27
|
}`
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
type WorkflowDocuments = {
|
|
30
|
+
workflowData: {
|
|
31
|
+
data: SanityDocumentWithMetadata[]
|
|
32
|
+
loading: boolean
|
|
33
|
+
error: boolean
|
|
34
|
+
}
|
|
35
|
+
operations: {
|
|
36
|
+
move: (
|
|
37
|
+
draggedId: string,
|
|
38
|
+
destination: DraggableLocation,
|
|
39
|
+
states: State[],
|
|
40
|
+
newOrder: string
|
|
41
|
+
) => void
|
|
42
|
+
}
|
|
29
43
|
}
|
|
30
44
|
|
|
31
|
-
export function useWorkflowDocuments(schemaTypes: string[]) {
|
|
45
|
+
export function useWorkflowDocuments(schemaTypes: string[]): WorkflowDocuments {
|
|
32
46
|
const toast = useToast()
|
|
33
|
-
const client = useClient()
|
|
47
|
+
const client = useClient({apiVersion: API_VERSION})
|
|
48
|
+
|
|
49
|
+
// Get and listen to changes on documents + workflow metadata documents
|
|
50
|
+
const {data, loading, error} = useListeningQuery<
|
|
51
|
+
SanityDocumentWithMetadata[]
|
|
52
|
+
>(QUERY, {
|
|
53
|
+
params: {schemaTypes},
|
|
54
|
+
initialValue: [],
|
|
55
|
+
})
|
|
56
|
+
|
|
34
57
|
const [localDocuments, setLocalDocuments] = React.useState<
|
|
35
58
|
SanityDocumentWithMetadata[]
|
|
36
59
|
>([])
|
|
37
60
|
|
|
38
|
-
// Get and listen to changes on documents + workflow metadata documents
|
|
39
|
-
const {data, loading, error} = useListeningQuery<DocumentsAndMetadata>(
|
|
40
|
-
COMBINED_QUERY,
|
|
41
|
-
{
|
|
42
|
-
params: {schemaTypes},
|
|
43
|
-
initialValue: INITIAL_DATA,
|
|
44
|
-
}
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
// Store local state for optimistic updates
|
|
48
61
|
React.useEffect(() => {
|
|
49
62
|
if (data) {
|
|
50
|
-
|
|
51
|
-
const documentsWithMetadata = data.documents.reduce(
|
|
52
|
-
(acc: SanityDocumentWithMetadata[], cur) => {
|
|
53
|
-
// Filter out documents without metadata
|
|
54
|
-
const curMeta = data.metadata.find(
|
|
55
|
-
(d) => d.documentId === cur._id.replace(`drafts.`, ``)
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
// Add _metadata as null so it can be shown as a document that needs to be imported into workflow
|
|
59
|
-
if (!curMeta) {
|
|
60
|
-
return [...acc, {_metadata: null, ...cur}]
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const curWithMetadata = {_metadata: curMeta, ...cur}
|
|
64
|
-
|
|
65
|
-
// Remove `published` from array if `draft` exists
|
|
66
|
-
if (!cur._id.startsWith(`drafts.`)) {
|
|
67
|
-
// eslint-disable-next-line max-nested-callbacks
|
|
68
|
-
const alsoHasDraft: boolean = Boolean(
|
|
69
|
-
data.documents.find((doc) => doc._id === `drafts.${cur._id}`)
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
return alsoHasDraft ? acc : [...acc, curWithMetadata]
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return [...acc, curWithMetadata]
|
|
76
|
-
},
|
|
77
|
-
[]
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
setLocalDocuments(documentsWithMetadata)
|
|
63
|
+
setLocalDocuments(data)
|
|
81
64
|
}
|
|
82
65
|
}, [data])
|
|
83
66
|
|
|
84
67
|
const move = React.useCallback(
|
|
85
|
-
(
|
|
68
|
+
async (
|
|
69
|
+
draggedId: string,
|
|
70
|
+
destination: DraggableLocation,
|
|
71
|
+
states: State[],
|
|
72
|
+
newOrder: string
|
|
73
|
+
) => {
|
|
86
74
|
// Optimistic update
|
|
87
75
|
const currentLocalData = localDocuments
|
|
88
76
|
const newLocalDocuments = localDocuments.map((item) => {
|
|
@@ -92,6 +80,12 @@ export function useWorkflowDocuments(schemaTypes: string[]) {
|
|
|
92
80
|
_metadata: {
|
|
93
81
|
...item._metadata,
|
|
94
82
|
state: destination.droppableId,
|
|
83
|
+
orderRank: newOrder,
|
|
84
|
+
// This value won't be written to the document
|
|
85
|
+
// It's done so that un/publish operations don't happen twice
|
|
86
|
+
// Because a moved document's card will update once optimistically
|
|
87
|
+
// and then again when the document is updated
|
|
88
|
+
optimistic: true,
|
|
95
89
|
},
|
|
96
90
|
}
|
|
97
91
|
}
|
|
@@ -128,29 +122,33 @@ export function useWorkflowDocuments(schemaTypes: string[]) {
|
|
|
128
122
|
const {_id, _type} = document
|
|
129
123
|
|
|
130
124
|
// Metadata + useDocumentOperation always uses Published id
|
|
131
|
-
const {
|
|
125
|
+
const {documentId, _rev} = document._metadata || {}
|
|
132
126
|
|
|
133
|
-
client
|
|
127
|
+
await client
|
|
134
128
|
.patch(`workflow-metadata.${documentId}`)
|
|
135
|
-
.ifRevisionId(_rev
|
|
136
|
-
.set({state: newStateId})
|
|
129
|
+
.ifRevisionId(_rev)
|
|
130
|
+
.set({state: newStateId, orderRank: newOrder})
|
|
137
131
|
.commit()
|
|
138
|
-
.then(() => {
|
|
139
|
-
|
|
140
|
-
title:
|
|
141
|
-
|
|
132
|
+
.then((res) => {
|
|
133
|
+
toast.push({
|
|
134
|
+
title:
|
|
135
|
+
newState.id === document._metadata.state
|
|
136
|
+
? `Reordered in "${newState?.title ?? newStateId}"`
|
|
137
|
+
: `Moved to "${newState?.title ?? newStateId}"`,
|
|
142
138
|
status: 'success',
|
|
143
139
|
})
|
|
140
|
+
return res
|
|
144
141
|
})
|
|
145
|
-
.catch(() => {
|
|
142
|
+
.catch((err) => {
|
|
146
143
|
// Revert optimistic update
|
|
147
144
|
setLocalDocuments(currentLocalData)
|
|
148
145
|
|
|
149
|
-
|
|
146
|
+
toast.push({
|
|
150
147
|
title: `Failed to move to "${newState?.title ?? newStateId}"`,
|
|
151
|
-
description:
|
|
148
|
+
description: err.message,
|
|
152
149
|
status: 'error',
|
|
153
150
|
})
|
|
151
|
+
return null
|
|
154
152
|
})
|
|
155
153
|
|
|
156
154
|
// Send back to the workflow board so a document update can happen
|
|
@@ -1,44 +1,49 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {useMemo} from 'react'
|
|
2
2
|
import {useListeningQuery} from 'sanity-plugin-utils'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {API_VERSION} from '../constants'
|
|
5
|
+
import {KeyedMetadata, Metadata} from '../types'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
* Takes the published ID of
|
|
8
|
+
* Takes the published ID of documents and return the metadata for those documents.
|
|
8
9
|
*
|
|
9
|
-
* @param
|
|
10
|
-
* @param states Array of States defined in plugin config
|
|
11
|
-
* @returns State
|
|
10
|
+
* @param ids Source document published IDs
|
|
12
11
|
*/
|
|
13
|
-
export function useWorkflowMetadata(
|
|
14
|
-
|
|
15
|
-
states: State[]
|
|
16
|
-
): {
|
|
17
|
-
data: {metadata?: Metadata; state?: State}
|
|
12
|
+
export function useWorkflowMetadata(ids: string[]): {
|
|
13
|
+
data: KeyedMetadata
|
|
18
14
|
loading: boolean
|
|
19
15
|
error: boolean
|
|
20
16
|
} {
|
|
21
17
|
const {
|
|
22
|
-
data:
|
|
18
|
+
data: rawData,
|
|
23
19
|
loading,
|
|
24
20
|
error,
|
|
25
|
-
} = useListeningQuery<Metadata>(
|
|
26
|
-
`*[_type == "workflow.metadata" && documentId
|
|
21
|
+
} = useListeningQuery<Metadata[]>(
|
|
22
|
+
`*[_type == "workflow.metadata" && documentId in $ids]{
|
|
23
|
+
_id,
|
|
24
|
+
_type,
|
|
25
|
+
_rev,
|
|
26
|
+
assignees,
|
|
27
|
+
documentId,
|
|
28
|
+
state,
|
|
29
|
+
orderRank
|
|
30
|
+
}`,
|
|
27
31
|
{
|
|
28
|
-
params: {
|
|
32
|
+
params: {ids},
|
|
33
|
+
options: {apiVersion: API_VERSION},
|
|
29
34
|
}
|
|
30
35
|
)
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
}
|
|
37
|
+
const keyedMetadata = useMemo(() => {
|
|
38
|
+
if (!rawData || rawData.length === 0) return {}
|
|
39
|
+
|
|
40
|
+
return rawData.reduce<KeyedMetadata>((acc, cur) => {
|
|
41
|
+
return {
|
|
42
|
+
...acc,
|
|
43
|
+
[cur.documentId]: cur,
|
|
44
|
+
}
|
|
45
|
+
}, {})
|
|
46
|
+
}, [rawData])
|
|
42
47
|
|
|
43
|
-
return {data:
|
|
48
|
+
return {data: keyedMetadata, loading, error}
|
|
44
49
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,47 +1,28 @@
|
|
|
1
|
-
import {definePlugin} from 'sanity'
|
|
2
|
-
import {CheckmarkIcon, SplitVerticalIcon} from '@sanity/icons'
|
|
1
|
+
import {definePlugin, DocumentActionProps, isObjectInputProps} from 'sanity'
|
|
3
2
|
|
|
4
|
-
import
|
|
3
|
+
import {AssignWorkflow} from './actions/AssignWorkflow'
|
|
4
|
+
import {BeginWorkflow} from './actions/BeginWorkflow'
|
|
5
|
+
import {CompleteWorkflow} from './actions/CompleteWorkflow'
|
|
6
|
+
import {UpdateWorkflow} from './actions/UpdateWorkflow'
|
|
7
|
+
import {AssigneesBadge} from './badges/AssigneesBadge'
|
|
8
|
+
import {StateBadge} from './badges/StateBadge'
|
|
9
|
+
import {WorkflowProvider} from './components/WorkflowContext'
|
|
10
|
+
import WorkflowSignal from './components/WorkflowSignal'
|
|
11
|
+
import {DEFAULT_CONFIG} from './constants'
|
|
12
|
+
import metadata from './schema/workflow/workflow.metadata'
|
|
13
|
+
import {workflowTool} from './tools'
|
|
5
14
|
import {WorkflowConfig} from './types'
|
|
6
|
-
import metadata from './schema/workflow/metadata'
|
|
7
|
-
import {StateBadge} from './badges'
|
|
8
|
-
import {PromoteAction} from './actions/PromoteAction'
|
|
9
|
-
import {DemoteAction} from './actions/DemoteAction'
|
|
10
|
-
//import StateTimeline from './components/StateTimeline'
|
|
11
|
-
|
|
12
|
-
const DEFAULT_CONFIG: WorkflowConfig = {
|
|
13
|
-
schemaTypes: [],
|
|
14
|
-
states: [
|
|
15
|
-
{id: 'draft', title: 'Draft', operation: 'unpublish'},
|
|
16
|
-
{id: 'inReview', title: 'In review', operation: null, color: 'primary'},
|
|
17
|
-
{
|
|
18
|
-
id: 'approved',
|
|
19
|
-
title: 'Approved',
|
|
20
|
-
operation: null,
|
|
21
|
-
color: 'success',
|
|
22
|
-
icon: CheckmarkIcon,
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
id: 'changesRequested',
|
|
26
|
-
title: 'Changes requested',
|
|
27
|
-
operation: null,
|
|
28
|
-
color: 'warning',
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
id: 'published',
|
|
32
|
-
title: 'Published',
|
|
33
|
-
operation: 'publish',
|
|
34
|
-
color: 'success',
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
}
|
|
38
15
|
|
|
39
16
|
export const workflow = definePlugin<WorkflowConfig>(
|
|
40
17
|
(config = DEFAULT_CONFIG) => {
|
|
41
18
|
const {schemaTypes, states} = {...DEFAULT_CONFIG, ...config}
|
|
42
19
|
|
|
43
20
|
if (!states?.length) {
|
|
44
|
-
throw new Error(`Workflow: Missing states in config`)
|
|
21
|
+
throw new Error(`Workflow plugin: Missing "states" in config`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!schemaTypes?.length) {
|
|
25
|
+
throw new Error(`Workflow plugin: Missing "schemaTypes" in config`)
|
|
45
26
|
}
|
|
46
27
|
|
|
47
28
|
return {
|
|
@@ -49,17 +30,29 @@ export const workflow = definePlugin<WorkflowConfig>(
|
|
|
49
30
|
schema: {
|
|
50
31
|
types: [metadata(states)],
|
|
51
32
|
},
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
33
|
+
// TODO: Remove 'workflow.metadata' from list of new document types
|
|
34
|
+
// ...
|
|
35
|
+
studio: {
|
|
36
|
+
components: {
|
|
37
|
+
layout: (props) =>
|
|
38
|
+
WorkflowProvider({...props, workflow: {schemaTypes, states}}),
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
form: {
|
|
42
|
+
components: {
|
|
43
|
+
input: (props) => {
|
|
44
|
+
if (
|
|
45
|
+
props.id === `root` &&
|
|
46
|
+
isObjectInputProps(props) &&
|
|
47
|
+
schemaTypes.includes(props.schemaType.name)
|
|
48
|
+
) {
|
|
49
|
+
return WorkflowSignal(props)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return props.renderDefault(props)
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
63
56
|
document: {
|
|
64
57
|
actions: (prev, context) => {
|
|
65
58
|
if (!schemaTypes.includes(context.schemaType)) {
|
|
@@ -67,8 +60,13 @@ export const workflow = definePlugin<WorkflowConfig>(
|
|
|
67
60
|
}
|
|
68
61
|
|
|
69
62
|
return [
|
|
70
|
-
(props) =>
|
|
71
|
-
(props) =>
|
|
63
|
+
(props) => BeginWorkflow(props),
|
|
64
|
+
(props) => AssignWorkflow(props),
|
|
65
|
+
...states.map(
|
|
66
|
+
(state) => (props: DocumentActionProps) =>
|
|
67
|
+
UpdateWorkflow(props, state)
|
|
68
|
+
),
|
|
69
|
+
(props) => CompleteWorkflow(props),
|
|
72
70
|
...prev,
|
|
73
71
|
]
|
|
74
72
|
},
|
|
@@ -77,17 +75,22 @@ export const workflow = definePlugin<WorkflowConfig>(
|
|
|
77
75
|
return prev
|
|
78
76
|
}
|
|
79
77
|
|
|
80
|
-
|
|
78
|
+
const {documentId, currentUser} = context
|
|
79
|
+
|
|
80
|
+
if (!documentId) {
|
|
81
|
+
return prev
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
() => StateBadge(documentId),
|
|
86
|
+
() => AssigneesBadge(documentId, currentUser),
|
|
87
|
+
...prev,
|
|
88
|
+
]
|
|
81
89
|
},
|
|
82
90
|
},
|
|
83
91
|
tools: [
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
title: 'Workflow',
|
|
87
|
-
component: WorkflowTool,
|
|
88
|
-
icon: SplitVerticalIcon,
|
|
89
|
-
options: {schemaTypes, states},
|
|
90
|
-
},
|
|
92
|
+
// TODO: These configs could be read from Context
|
|
93
|
+
workflowTool({schemaTypes, states}),
|
|
91
94
|
],
|
|
92
95
|
}
|
|
93
96
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {defineField, defineType} from 'sanity'
|
|
2
|
+
|
|
3
|
+
import Field from '../../components/DocumentCard/Field'
|
|
4
|
+
import UserAssignmentInput from '../../components/UserAssignmentInput'
|
|
5
|
+
import {API_VERSION} from '../../constants'
|
|
6
|
+
import initialRank from '../../helpers/initialRank'
|
|
7
|
+
import {State} from '../../types'
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
10
|
+
export default (states: State[]) =>
|
|
11
|
+
defineType({
|
|
12
|
+
type: 'document',
|
|
13
|
+
name: 'workflow.metadata',
|
|
14
|
+
title: 'Workflow metadata',
|
|
15
|
+
liveEdit: true,
|
|
16
|
+
fields: [
|
|
17
|
+
defineField({
|
|
18
|
+
name: 'state',
|
|
19
|
+
description: `The current "State" of the document. Field is read only as changing it would not fire the state's "operation" setting. These are fired in the Document Actions and in the custom Tool.`,
|
|
20
|
+
readOnly: true,
|
|
21
|
+
type: 'string',
|
|
22
|
+
options: {
|
|
23
|
+
list: states.length
|
|
24
|
+
? states.map((state) => ({
|
|
25
|
+
value: state.id,
|
|
26
|
+
title: state.title,
|
|
27
|
+
}))
|
|
28
|
+
: [],
|
|
29
|
+
layout: 'radio',
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
defineField({
|
|
33
|
+
name: 'documentId',
|
|
34
|
+
title: 'Document ID',
|
|
35
|
+
description:
|
|
36
|
+
'Used to help identify the target document that this metadata is tracking state for.',
|
|
37
|
+
type: 'string',
|
|
38
|
+
readOnly: true,
|
|
39
|
+
components: {
|
|
40
|
+
input: Field,
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
defineField({
|
|
44
|
+
name: 'orderRank',
|
|
45
|
+
description: 'Used to maintain order position of cards in the Tool.',
|
|
46
|
+
type: 'string',
|
|
47
|
+
readOnly: true,
|
|
48
|
+
initialValue: async (p, {getClient}) => {
|
|
49
|
+
const lastDocOrderRank = await getClient({
|
|
50
|
+
apiVersion: API_VERSION,
|
|
51
|
+
}).fetch(`*[_type == $type]|order(@[$order] desc)[0][$order]`, {
|
|
52
|
+
order: `orderRank`,
|
|
53
|
+
type: `workflow.metadata`,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return initialRank(lastDocOrderRank)
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
defineField({
|
|
60
|
+
type: 'array',
|
|
61
|
+
name: 'assignees',
|
|
62
|
+
of: [{type: 'string'}],
|
|
63
|
+
components: {
|
|
64
|
+
input: UserAssignmentInput,
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
],
|
|
68
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {SplitVerticalIcon} from '@sanity/icons'
|
|
2
|
+
import {Tool} from 'sanity'
|
|
3
|
+
|
|
4
|
+
import WorkflowTool from '../components/WorkflowTool'
|
|
5
|
+
import {WorkflowConfig} from '../types'
|
|
6
|
+
|
|
7
|
+
export type WorkflowToolConfig = (options: WorkflowConfig) => Tool
|
|
8
|
+
|
|
9
|
+
export const workflowTool: WorkflowToolConfig = (options: WorkflowConfig) => ({
|
|
10
|
+
name: 'workflow',
|
|
11
|
+
title: 'Workflow',
|
|
12
|
+
component: WorkflowTool,
|
|
13
|
+
icon: SplitVerticalIcon,
|
|
14
|
+
options,
|
|
15
|
+
})
|
package/src/types/index.ts
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
1
|
import {SanityDocumentLike} from 'sanity'
|
|
3
2
|
|
|
4
3
|
export type State = {
|
|
5
4
|
id: string
|
|
5
|
+
transitions: string[]
|
|
6
6
|
title: string
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
roles?: string[]
|
|
8
|
+
requireAssignment?: boolean
|
|
9
|
+
requireValidation?: boolean
|
|
10
|
+
// From document badges
|
|
9
11
|
color?: 'primary' | 'success' | 'warning' | 'danger'
|
|
10
|
-
icon?: React.ReactNode | React.ComponentType
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
export type StateCheck<Id, States> = {
|
|
15
|
+
id: Id
|
|
16
|
+
// Transitions is an array of State ids
|
|
17
|
+
transitions?: States extends {id: infer Id2}[] ? Id2[] : never
|
|
18
|
+
} & State
|
|
19
|
+
|
|
13
20
|
export type WorkflowConfig = {
|
|
14
21
|
schemaTypes: string[]
|
|
15
22
|
states?: State[]
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
export function defineStates<
|
|
26
|
+
Id extends string,
|
|
27
|
+
States extends StateCheck<Id, States>[]
|
|
28
|
+
>(states: States): States {
|
|
29
|
+
return states
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
export type User = {
|
|
19
33
|
createdAt: string
|
|
20
34
|
displayName: string
|
|
@@ -43,8 +57,15 @@ export type Metadata = SanityDocumentLike & {
|
|
|
43
57
|
assignees: string[]
|
|
44
58
|
documentId: string
|
|
45
59
|
state: string
|
|
60
|
+
orderRank: string
|
|
46
61
|
}
|
|
47
62
|
|
|
48
|
-
export type
|
|
49
|
-
|
|
63
|
+
export type KeyedMetadata = {[key: string]: Metadata}
|
|
64
|
+
|
|
65
|
+
export type SanityDocumentWithMetadata = {
|
|
66
|
+
_metadata: Metadata
|
|
67
|
+
_id: string
|
|
68
|
+
_type: string
|
|
69
|
+
_rev: string
|
|
70
|
+
_updatedAt: string
|
|
50
71
|
}
|