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.
Files changed (56) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +81 -13
  3. package/lib/{src/index.d.ts → index.d.ts} +4 -3
  4. package/lib/index.esm.js +2106 -1
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +2119 -1
  7. package/lib/index.js.map +1 -1
  8. package/package.json +51 -40
  9. package/src/actions/AssignWorkflow.tsx +49 -0
  10. package/src/actions/BeginWorkflow.tsx +63 -0
  11. package/src/actions/CompleteWorkflow.tsx +41 -0
  12. package/src/actions/UpdateWorkflow.tsx +126 -0
  13. package/src/badges/AssigneesBadge.tsx +53 -0
  14. package/src/badges/StateBadge.tsx +28 -0
  15. package/src/components/DocumentCard/AvatarGroup.tsx +12 -8
  16. package/src/components/DocumentCard/CompleteButton.tsx +68 -0
  17. package/src/components/DocumentCard/EditButton.tsx +3 -2
  18. package/src/components/DocumentCard/Field.tsx +38 -0
  19. package/src/components/DocumentCard/Validate.tsx +21 -0
  20. package/src/components/DocumentCard/ValidationStatus.tsx +37 -0
  21. package/src/components/DocumentCard/core/DraftStatus.tsx +32 -0
  22. package/src/components/DocumentCard/core/PublishedStatus.tsx +39 -0
  23. package/src/components/DocumentCard/core/TimeAgo.tsx +11 -0
  24. package/src/components/DocumentCard/index.tsx +177 -68
  25. package/src/components/DocumentList.tsx +169 -0
  26. package/src/components/Filters.tsx +168 -0
  27. package/src/components/FloatingCard.tsx +29 -0
  28. package/src/components/StateTitle/Status.tsx +27 -0
  29. package/src/components/StateTitle/index.tsx +78 -0
  30. package/src/components/UserAssignment.tsx +57 -75
  31. package/src/components/UserAssignmentInput.tsx +27 -0
  32. package/src/components/UserDisplay.tsx +57 -0
  33. package/src/components/Verify.tsx +297 -0
  34. package/src/components/WorkflowContext.tsx +71 -0
  35. package/src/components/WorkflowSignal.tsx +30 -0
  36. package/src/components/WorkflowTool.tsx +373 -162
  37. package/src/constants/index.ts +31 -0
  38. package/src/helpers/arraysContainMatchingString.ts +6 -0
  39. package/src/helpers/filterItemsAndSort.ts +41 -0
  40. package/src/helpers/generateMultipleOrderRanks.ts +80 -0
  41. package/src/helpers/initialRank.ts +13 -0
  42. package/src/hooks/useWorkflowDocuments.tsx +76 -78
  43. package/src/hooks/useWorkflowMetadata.tsx +31 -26
  44. package/src/index.ts +60 -57
  45. package/src/schema/workflow/workflow.metadata.ts +68 -0
  46. package/src/tools/index.ts +15 -0
  47. package/src/types/index.ts +27 -6
  48. package/src/actions/DemoteAction.tsx +0 -62
  49. package/src/actions/PromoteAction.tsx +0 -62
  50. package/src/actions/RequestReviewAction.js +0 -61
  51. package/src/actions/index.js +0 -21
  52. package/src/badges/index.tsx +0 -31
  53. package/src/components/Mutate.tsx +0 -54
  54. package/src/components/StateTimeline.tsx +0 -98
  55. package/src/components/UserSelectInput.tsx +0 -43
  56. 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
- const DOCUMENT_LIST_QUERY = `*[_type in $schemaTypes]{ _id, _type, _rev }`
14
- const METADATA_LIST_QUERY = `*[_type == "workflow.metadata"]{
15
- _rev,
16
- assignees,
17
- documentId,
18
- state
19
- }`
20
-
21
- const COMBINED_QUERY = `{
22
- "documents": ${DOCUMENT_LIST_QUERY},
23
- "metadata": ${METADATA_LIST_QUERY}
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
- const INITIAL_DATA: DocumentsAndMetadata = {
27
- documents: [],
28
- metadata: [],
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
- // Combine metadata data into document
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
- (draggedId: string, destination: DraggableLocation, states: State[]) => {
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 {_rev, documentId} = document._metadata || {}
125
+ const {documentId, _rev} = document._metadata || {}
132
126
 
133
- client
127
+ await client
134
128
  .patch(`workflow-metadata.${documentId}`)
135
- .ifRevisionId(_rev as string)
136
- .set({state: newStateId})
129
+ .ifRevisionId(_rev)
130
+ .set({state: newStateId, orderRank: newOrder})
137
131
  .commit()
138
- .then(() => {
139
- return toast.push({
140
- title: `Moved to "${newState?.title ?? newStateId}"`,
141
- description: documentId,
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
- return toast.push({
146
+ toast.push({
150
147
  title: `Failed to move to "${newState?.title ?? newStateId}"`,
151
- description: documentId,
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 React from 'react'
1
+ import {useMemo} from 'react'
2
2
  import {useListeningQuery} from 'sanity-plugin-utils'
3
3
 
4
- import {Metadata, State} from '../types'
4
+ import {API_VERSION} from '../constants'
5
+ import {KeyedMetadata, Metadata} from '../types'
5
6
 
6
7
  /**
7
- * Takes the published ID of a document and return the metadata and current state object
8
+ * Takes the published ID of documents and return the metadata for those documents.
8
9
  *
9
- * @param id Source document published ID
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
- id: string,
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: metadata,
18
+ data: rawData,
23
19
  loading,
24
20
  error,
25
- } = useListeningQuery<Metadata>(
26
- `*[_type == "workflow.metadata" && documentId == $id][0]`,
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: {id},
32
+ params: {ids},
33
+ options: {apiVersion: API_VERSION},
29
34
  }
30
35
  )
31
36
 
32
- if (metadata?.state) {
33
- return {
34
- data: {
35
- metadata,
36
- state: states.find((s) => s.id === metadata.state),
37
- },
38
- loading,
39
- error,
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: {}, loading, error}
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 WorkflowTool from './components/WorkflowTool'
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
- // form: {
53
- // components: {
54
- // item: (props) => {
55
- // console.log(props)
56
- // // if (props.id === `root` && schemaTypes.includes(props.schemaType.name)) {
57
- // // return StateTimeline(props)
58
- // // }
59
- // return props.renderDefault(props)
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) => PromoteAction(props, states),
71
- (props) => DemoteAction(props, states),
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
- return [(props) => StateBadge(props, states), ...prev]
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
- name: 'workflow',
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
+ })
@@ -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
- operation?: 'publish' | 'unpublish' | null
8
- // From badge props
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 SanityDocumentWithMetadata = SanityDocumentLike & {
49
- _metadata: Metadata | null
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
  }