sanity-plugin-workflow 1.0.0-beta.4 → 1.0.0-beta.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sanity-plugin-workflow",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "A demonstration of a custom content publishing workflow using Sanity.",
5
5
  "keywords": [
6
6
  "sanity",
@@ -1,25 +1,20 @@
1
- // import {useState} from 'react'
2
- import {ArrowRightIcon, ArrowLeftIcon} from '@sanity/icons'
1
+ import {ArrowLeftIcon, ArrowRightIcon} from '@sanity/icons'
3
2
  import {useToast} from '@sanity/ui'
4
3
  import {useCurrentUser, useValidationStatus} from 'sanity'
5
4
  import {DocumentActionProps, useClient} from 'sanity'
6
5
 
7
- import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
8
6
  import {API_VERSION} from '../constants'
9
- import {State} from '../types'
10
7
  import {arraysContainMatchingString} from '../helpers/arraysContainMatchingString'
8
+ import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
9
+ import {State} from '../types'
11
10
 
11
+ // eslint-disable-next-line complexity
12
12
  export function UpdateWorkflow(
13
13
  props: DocumentActionProps,
14
14
  allStates: State[],
15
15
  actionState: State
16
16
  ) {
17
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
18
 
24
19
  const user = useCurrentUser()
25
20
  const client = useClient({apiVersion: API_VERSION})
@@ -30,6 +25,13 @@ export function UpdateWorkflow(
30
25
  const {state: currentState} = data
31
26
  const {assignees = []} = data?.metadata ?? {}
32
27
 
28
+ const {validation, isValidating} = useValidationStatus(id, type)
29
+ const hasValidationErrors =
30
+ currentState?.requireValidation &&
31
+ !isValidating &&
32
+ validation?.length > 0 &&
33
+ validation.find((v) => v.level === 'error')
34
+
33
35
  if (error) {
34
36
  console.error(error)
35
37
  }
@@ -80,8 +82,6 @@ export function UpdateWorkflow(
80
82
  const DirectionIcon = direction === 'promote' ? ArrowRightIcon : ArrowLeftIcon
81
83
  const directionLabel = direction === 'promote' ? 'Promote' : 'Demote'
82
84
 
83
- let title = `${directionLabel} State to "${actionState.title}"`
84
-
85
85
  const userRoleCanUpdateState =
86
86
  user?.roles?.length && actionState?.roles?.length
87
87
  ? // If the Action state is limited to specific roles
@@ -93,10 +93,6 @@ export function UpdateWorkflow(
93
93
  : // No roles specified on the next state, so anyone can update
94
94
  actionState?.roles?.length !== 0
95
95
 
96
- if (!userRoleCanUpdateState) {
97
- title = `Your User role cannot ${directionLabel} State to "${actionState.title}"`
98
- }
99
-
100
96
  const actionStateIsAValidTransition =
101
97
  currentState?.id && currentState.transitions.length
102
98
  ? // If the Current State limits transitions to specific States
@@ -105,10 +101,6 @@ export function UpdateWorkflow(
105
101
  : // Otherwise this isn't a problem
106
102
  true
107
103
 
108
- if (!actionStateIsAValidTransition) {
109
- title = `You cannot ${directionLabel} State to "${actionState.title}" from "${currentState?.title}"`
110
- }
111
-
112
104
  const userAssignmentCanUpdateState = actionState.requireAssignment
113
105
  ? // If the Action State requires assigned users
114
106
  // Check the current user ID is in the assignees array
@@ -116,11 +108,17 @@ export function UpdateWorkflow(
116
108
  : // Otherwise this isn't a problem
117
109
  true
118
110
 
119
- if (!userAssignmentCanUpdateState) {
120
- title = `You must be assigned to the document to ${directionLabel} State to "${actionState.title}"`
121
- }
111
+ let title = `${directionLabel} State to "${actionState.title}"`
122
112
 
123
- if (hasValidationErrors) {
113
+ if (!userRoleCanUpdateState) {
114
+ title = `Your User role cannot ${directionLabel} State to "${actionState.title}"`
115
+ } else if (!actionStateIsAValidTransition) {
116
+ title = `You cannot ${directionLabel} State to "${actionState.title}" from "${currentState?.title}"`
117
+ } else if (!userAssignmentCanUpdateState) {
118
+ title = `You must be assigned to the document to ${directionLabel} State to "${actionState.title}"`
119
+ } else if (currentState?.requireValidation && isValidating) {
120
+ title = `Document is validating, cannot ${directionLabel} State to "${actionState.title}"`
121
+ } else if (hasValidationErrors) {
124
122
  title = `Document has validation errors, cannot ${directionLabel} State to "${actionState.title}"`
125
123
  }
126
124
 
@@ -129,7 +127,7 @@ export function UpdateWorkflow(
129
127
  disabled:
130
128
  loading ||
131
129
  error ||
132
- isValidating ||
130
+ (currentState?.requireValidation && isValidating) ||
133
131
  hasValidationErrors ||
134
132
  !currentState ||
135
133
  !userRoleCanUpdateState ||
@@ -0,0 +1,21 @@
1
+ import {useEffect} from 'react'
2
+ import {useValidationStatus, ValidationStatus} from 'sanity'
3
+
4
+ type ValidateProps = {
5
+ documentId: string
6
+ type: string
7
+ onChange: (validation: ValidationStatus) => void
8
+ }
9
+
10
+ // Document validation is siloed into its own component
11
+ // Because it's not performant to run on a lot of documents
12
+ export default function Validate(props: ValidateProps) {
13
+ const {documentId, type, onChange} = props
14
+ const {isValidating, validation = []} = useValidationStatus(documentId, type)
15
+
16
+ useEffect(() => {
17
+ onChange({isValidating, validation})
18
+ }, [onChange, isValidating, validation])
19
+
20
+ return null
21
+ }
@@ -1,8 +1,12 @@
1
1
  /* eslint-disable react/prop-types */
2
2
  import {DragHandleIcon} from '@sanity/icons'
3
3
  import {Box, Card, CardTone, Flex, Stack, useTheme} from '@sanity/ui'
4
- import {useEffect, useMemo} from 'react'
5
- import {SchemaType, useSchema, useValidationStatus} from 'sanity'
4
+ import {useCallback, useEffect, useMemo, useState} from 'react'
5
+ import {
6
+ SchemaType,
7
+ useSchema,
8
+ ValidationStatus as ValidationStatusType,
9
+ } from 'sanity'
6
10
  import {Preview} from 'sanity'
7
11
 
8
12
  import {SanityDocumentWithMetadata, State, User} from '../../types'
@@ -11,6 +15,7 @@ import CompleteButton from './CompleteButton'
11
15
  import {DraftStatus} from './core/DraftStatus'
12
16
  import {PublishedStatus} from './core/PublishedStatus'
13
17
  import EditButton from './EditButton'
18
+ import Validate from './Validate'
14
19
  import {ValidationStatus} from './ValidationStatus'
15
20
 
16
21
  type DocumentCardProps = {
@@ -38,6 +43,7 @@ export function DocumentCard(props: DocumentCardProps) {
38
43
  } = props
39
44
  const {assignees = [], documentId} = item._metadata ?? {}
40
45
  const schema = useSchema()
46
+ const state = states.find((s) => s.id === item._metadata?.state)
41
47
 
42
48
  // Perform document operations after State changes
43
49
  // If State has changed and the document needs to be un/published
@@ -80,10 +86,21 @@ export function DocumentCard(props: DocumentCardProps) {
80
86
 
81
87
  const isDarkMode = useTheme().sanity.color.dark
82
88
  const defaultCardTone = isDarkMode ? `transparent` : `default`
83
- const {validation = [], isValidating} = useValidationStatus(
84
- documentId ?? ``,
85
- item._type
86
- )
89
+
90
+ // Validation only runs if the state requests it
91
+ // Because it's not performant to run it on many documents simultaneously
92
+ // So we fake it here, and maybe set it inside <Validate />
93
+ const [optimisticValidation, setOptimisticValidation] =
94
+ useState<ValidationStatusType>({
95
+ isValidating: state?.requireValidation ?? false,
96
+ validation: [],
97
+ })
98
+
99
+ const {isValidating, validation} = optimisticValidation
100
+
101
+ const handleValidation = useCallback((updates: ValidationStatusType) => {
102
+ setOptimisticValidation(updates)
103
+ }, [])
87
104
 
88
105
  const cardTone = useMemo(() => {
89
106
  let tone: CardTone = defaultCardTone
@@ -92,7 +109,7 @@ export function DocumentCard(props: DocumentCardProps) {
92
109
  if (!documentId) return tone
93
110
  if (isDragging) tone = `positive`
94
111
 
95
- if (!isValidating && validation.length > 0) {
112
+ if (state?.requireValidation && !isValidating && validation.length > 0) {
96
113
  if (validation.some((v) => v.level === 'error')) {
97
114
  tone = `critical`
98
115
  } else {
@@ -102,13 +119,14 @@ export function DocumentCard(props: DocumentCardProps) {
102
119
 
103
120
  return tone
104
121
  }, [
105
- isDarkMode,
106
- userRoleCanDrop,
107
122
  defaultCardTone,
123
+ userRoleCanDrop,
124
+ isDarkMode,
108
125
  documentId,
109
126
  isDragging,
110
- validation,
111
127
  isValidating,
128
+ validation,
129
+ state?.requireValidation,
112
130
  ])
113
131
 
114
132
  // Update validation status
@@ -136,63 +154,72 @@ export function DocumentCard(props: DocumentCardProps) {
136
154
  )
137
155
 
138
156
  return (
139
- <Box paddingBottom={3} paddingX={3}>
140
- <Card radius={2} shadow={isDragging ? 3 : 1} tone={cardTone}>
141
- <Stack>
142
- <Card
143
- borderBottom
144
- radius={2}
145
- padding={3}
146
- paddingLeft={2}
147
- tone={cardTone}
148
- style={{pointerEvents: 'none'}}
149
- >
150
- <Flex align="center" justify="space-between" gap={1}>
151
- <Box flex={1}>
152
- <Preview
153
- layout="default"
154
- value={item}
155
- schemaType={schema.get(item._type) as SchemaType}
157
+ <>
158
+ {state?.requireValidation ? (
159
+ <Validate
160
+ documentId={documentId}
161
+ type={item._type}
162
+ onChange={handleValidation}
163
+ />
164
+ ) : null}
165
+ <Box paddingBottom={3} paddingX={3}>
166
+ <Card radius={2} shadow={isDragging ? 3 : 1} tone={cardTone}>
167
+ <Stack>
168
+ <Card
169
+ borderBottom
170
+ radius={2}
171
+ padding={3}
172
+ paddingLeft={2}
173
+ tone={cardTone}
174
+ style={{pointerEvents: 'none'}}
175
+ >
176
+ <Flex align="center" justify="space-between" gap={1}>
177
+ <Box flex={1}>
178
+ <Preview
179
+ layout="default"
180
+ value={item}
181
+ schemaType={schema.get(item._type) as SchemaType}
182
+ />
183
+ </Box>
184
+ <Box style={{flexShrink: 0}}>
185
+ {hasError || isDragDisabled ? null : <DragHandleIcon />}
186
+ </Box>
187
+ </Flex>
188
+ </Card>
189
+
190
+ <Card padding={2} radius={2} tone="inherit">
191
+ <Flex align="center" justify="space-between" gap={3}>
192
+ <Box flex={1}>
193
+ {documentId && (
194
+ <UserDisplay
195
+ userList={userList}
196
+ assignees={assignees}
197
+ documentId={documentId}
198
+ disabled={!userRoleCanDrop}
199
+ />
200
+ )}
201
+ </Box>
202
+ {validation.length > 0 ? (
203
+ <ValidationStatus validation={validation} />
204
+ ) : null}
205
+ <DraftStatus document={item} />
206
+ <PublishedStatus document={item} />
207
+ <EditButton
208
+ id={item._id}
209
+ type={item._type}
210
+ disabled={!userRoleCanDrop}
156
211
  />
157
- </Box>
158
- <Box style={{flexShrink: 0}}>
159
- {hasError || isDragDisabled ? null : <DragHandleIcon />}
160
- </Box>
161
- </Flex>
162
- </Card>
163
-
164
- <Card padding={2} radius={2} tone="inherit">
165
- <Flex align="center" justify="space-between" gap={3}>
166
- <Box flex={1}>
167
- {documentId && (
168
- <UserDisplay
169
- userList={userList}
170
- assignees={assignees}
212
+ {isLastState ? (
213
+ <CompleteButton
171
214
  documentId={documentId}
172
215
  disabled={!userRoleCanDrop}
173
216
  />
174
- )}
175
- </Box>
176
- {validation.length > 0 ? (
177
- <ValidationStatus validation={validation} />
178
- ) : null}
179
- <DraftStatus document={item} />
180
- <PublishedStatus document={item} />
181
- <EditButton
182
- id={item._id}
183
- type={item._type}
184
- disabled={!userRoleCanDrop}
185
- />
186
- {isLastState ? (
187
- <CompleteButton
188
- documentId={documentId}
189
- disabled={!userRoleCanDrop}
190
- />
191
- ) : null}
192
- </Flex>
193
- </Card>
194
- </Stack>
195
- </Card>
196
- </Box>
217
+ ) : null}
218
+ </Flex>
219
+ </Card>
220
+ </Stack>
221
+ </Card>
222
+ </Box>
223
+ </>
197
224
  )
198
225
  }
@@ -8,13 +8,17 @@ import {API_VERSION} from '../constants'
8
8
  import {SanityDocumentWithMetadata, State} from '../types'
9
9
  import FloatingCard from './FloatingCard'
10
10
 
11
- type ValidatorsProps = {
11
+ type VerifyProps = {
12
12
  data: SanityDocumentWithMetadata[]
13
13
  userList: UserExtended[]
14
14
  states: State[]
15
15
  }
16
16
 
17
- export default function Validators({data, userList, states}: ValidatorsProps) {
17
+ // This component checks the validity of the data in the Kanban
18
+ // It will only render something it there is invalid date
19
+ // And will render buttons to fix the data
20
+ export default function Verify(props: VerifyProps) {
21
+ const {data, userList, states} = props
18
22
  const client = useClient({apiVersion: API_VERSION})
19
23
  const toast = useToast()
20
24
 
@@ -19,7 +19,7 @@ import {DocumentCard} from './DocumentCard'
19
19
  import DocumentList from './DocumentList'
20
20
  import Filters from './Filters'
21
21
  import StateTitle from './StateTitle'
22
- import Validators from './Validators'
22
+ import Verify from './Verify'
23
23
 
24
24
  type WorkflowToolProps = {
25
25
  tool: Tool<WorkflowConfig>
@@ -155,12 +155,12 @@ export default function WorkflowTool(props: WorkflowToolProps) {
155
155
  // Must be between two items
156
156
  const itemBefore = destinationStateItems[destination.index - 1]
157
157
  const itemBeforeRank = itemBefore?._metadata?.orderRank
158
- const itemBeforeRankParsed = itemBefore._metadata.orderRank
158
+ const itemBeforeRankParsed = itemBeforeRank
159
159
  ? LexoRank.parse(itemBeforeRank)
160
160
  : LexoRank.min()
161
161
  const itemAfter = destinationStateItems[destination.index]
162
162
  const itemAfterRank = itemAfter?._metadata?.orderRank
163
- const itemAfterRankParsed = itemAfter._metadata.orderRank
163
+ const itemAfterRankParsed = itemAfterRank
164
164
  ? LexoRank.parse(itemAfterRank)
165
165
  : LexoRank.max()
166
166
 
@@ -249,7 +249,8 @@ export default function WorkflowTool(props: WorkflowToolProps) {
249
249
 
250
250
  return (
251
251
  <Flex direction="column" height="fill" overflow="hidden">
252
- <Validators data={data} userList={userList} states={states} />
252
+ <Verify data={data} userList={userList} states={states} />
253
+
253
254
  <Filters
254
255
  uniqueAssignedUsers={uniqueAssignedUsers}
255
256
  selectedUserIds={selectedUserIds}
@@ -289,6 +290,7 @@ export default function WorkflowTool(props: WorkflowToolProps) {
289
290
  isDropDisabled={isDropDisabled}
290
291
  // props required for virtualization
291
292
  mode="virtual"
293
+ // TODO: Render this as a memo/callback
292
294
  renderClone={(provided, snapshot, rubric) => {
293
295
  const item = data.find(
294
296
  (doc) =>
@@ -24,8 +24,8 @@ export const DEFAULT_CONFIG: WorkflowConfig = {
24
24
  title: 'Approved',
25
25
  color: 'success',
26
26
  roles: ['administrator'],
27
- requireAssignment: true,
28
27
  transitions: ['changesRequested'],
28
+ requireAssignment: true,
29
29
  },
30
30
  ]),
31
31
  }
@@ -9,6 +9,7 @@ export type State = {
9
9
  // operation?: Operation
10
10
  roles?: string[]
11
11
  requireAssignment?: boolean
12
+ requireValidation?: boolean
12
13
  // From document badges
13
14
  color?: 'primary' | 'success' | 'warning' | 'danger'
14
15
  }
@@ -1,55 +0,0 @@
1
- import PropTypes from 'prop-types'
2
- import React from 'react'
3
- import {EyeOpenIcon} from '@sanity/icons'
4
-
5
- import {inferMetadataState, useWorkflowMetadata} from '../../lib/workflow'
6
- import RequestReviewWizard from '../../components/RequestReviewWizard'
7
-
8
- export function RequestReviewAction(props) {
9
- const [showWizardDialog, setShowWizardDialog] = React.useState(false)
10
- const metadata = useWorkflowMetadata(props.id, inferMetadataState(props))
11
- const {state} = metadata.data
12
-
13
- if (!props.draft || state === 'inReview' || state === 'approved') {
14
- return null
15
- }
16
-
17
- const onHandle = () => {
18
- if (!showWizardDialog) {
19
- setShowWizardDialog(true)
20
- }
21
- }
22
-
23
- const onSend = (assignees) => {
24
- setShowWizardDialog(false)
25
-
26
- if (assignees.length === 0) {
27
- metadata.clearAssignees()
28
- } else {
29
- metadata.setAssignees(assignees)
30
- }
31
-
32
- metadata.setState('inReview')
33
- props.onComplete()
34
- }
35
-
36
- const onClose = () => setShowWizardDialog(false)
37
-
38
- return {
39
- dialog: showWizardDialog && {
40
- type: 'popover',
41
- content: <RequestReviewWizard metadata={metadata.data} onClose={onClose} onSend={onSend} />,
42
- onClose: props.onComplete,
43
- },
44
- disabled: showWizardDialog,
45
- icon: EyeOpenIcon,
46
- label: 'Request review',
47
- onHandle,
48
- }
49
- }
50
-
51
- RequestReviewAction.propTypes = {
52
- draft: PropTypes.object,
53
- id: PropTypes.string,
54
- onComplete: PropTypes.func,
55
- }
@@ -1,21 +0,0 @@
1
- import {ApproveAction} from './ApproveAction'
2
- import {DeleteAction} from './DeleteAction'
3
- import {DiscardChangesAction} from './DiscardChangesAction'
4
- import {PublishAction} from './PublishAction'
5
- import {RequestChangesAction} from './RequestChangesAction'
6
- import {RequestReviewAction} from './RequestReviewAction'
7
- import {SyncAction} from './SyncAction'
8
- import {UnpublishAction} from './Unpublish'
9
-
10
- export function resolveWorkflowActions(/* docInfo */) {
11
- return [
12
- SyncAction,
13
- RequestReviewAction,
14
- ApproveAction,
15
- RequestChangesAction,
16
- PublishAction,
17
- UnpublishAction,
18
- DiscardChangesAction,
19
- DeleteAction,
20
- ]
21
- }