sanity-plugin-workflow 1.0.6 → 2.0.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.
Files changed (52) hide show
  1. package/README.md +2 -17
  2. package/dist/index.d.ts +17 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +1647 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +32 -71
  7. package/lib/index.d.ts +0 -20
  8. package/lib/index.esm.js +0 -2135
  9. package/lib/index.esm.js.map +0 -1
  10. package/lib/index.js +0 -2147
  11. package/lib/index.js.map +0 -1
  12. package/sanity.json +0 -8
  13. package/src/actions/AssignWorkflow.tsx +0 -47
  14. package/src/actions/BeginWorkflow.tsx +0 -63
  15. package/src/actions/CompleteWorkflow.tsx +0 -64
  16. package/src/actions/UpdateWorkflow.tsx +0 -126
  17. package/src/badges/AssigneesBadge.tsx +0 -53
  18. package/src/badges/StateBadge.tsx +0 -28
  19. package/src/components/DocumentCard/AvatarGroup.tsx +0 -43
  20. package/src/components/DocumentCard/CompleteButton.tsx +0 -56
  21. package/src/components/DocumentCard/EditButton.tsx +0 -28
  22. package/src/components/DocumentCard/Field.tsx +0 -38
  23. package/src/components/DocumentCard/Validate.tsx +0 -21
  24. package/src/components/DocumentCard/ValidationStatus.tsx +0 -37
  25. package/src/components/DocumentCard/core/DraftStatus.tsx +0 -32
  26. package/src/components/DocumentCard/core/PublishedStatus.tsx +0 -39
  27. package/src/components/DocumentCard/core/TimeAgo.tsx +0 -11
  28. package/src/components/DocumentCard/index.tsx +0 -200
  29. package/src/components/DocumentList.tsx +0 -169
  30. package/src/components/Filters.tsx +0 -174
  31. package/src/components/FloatingCard.tsx +0 -36
  32. package/src/components/StateTitle/Status.tsx +0 -27
  33. package/src/components/StateTitle/index.tsx +0 -78
  34. package/src/components/UserAssignment.tsx +0 -121
  35. package/src/components/UserAssignmentInput.tsx +0 -27
  36. package/src/components/UserDisplay.tsx +0 -57
  37. package/src/components/Verify.tsx +0 -297
  38. package/src/components/WorkflowContext.tsx +0 -71
  39. package/src/components/WorkflowSignal.tsx +0 -30
  40. package/src/components/WorkflowTool.tsx +0 -437
  41. package/src/constants/index.ts +0 -31
  42. package/src/helpers/arraysContainMatchingString.ts +0 -6
  43. package/src/helpers/filterItemsAndSort.ts +0 -41
  44. package/src/helpers/generateMultipleOrderRanks.ts +0 -80
  45. package/src/helpers/initialRank.ts +0 -13
  46. package/src/hooks/useWorkflowDocuments.tsx +0 -167
  47. package/src/hooks/useWorkflowMetadata.tsx +0 -49
  48. package/src/index.ts +0 -97
  49. package/src/schema/workflow/workflow.metadata.ts +0 -68
  50. package/src/tools/index.ts +0 -15
  51. package/src/types/index.ts +0 -71
  52. package/v2-incompatible.js +0 -11
@@ -1,121 +0,0 @@
1
- import React from 'react'
2
- import {useToast} from '@sanity/ui'
3
- import {UserSelectMenu} from 'sanity-plugin-utils'
4
- import {useClient} from 'sanity'
5
-
6
- import {User} from '../types'
7
- import {API_VERSION} from '../constants'
8
-
9
- type UserAssignmentProps = {
10
- userList: User[]
11
- assignees: string[]
12
- documentId: string
13
- }
14
-
15
- export default function UserAssignment(props: UserAssignmentProps) {
16
- const {assignees, userList, documentId} = props
17
- const client = useClient({apiVersion: API_VERSION})
18
- const toast = useToast()
19
-
20
- const addAssignee = React.useCallback(
21
- (userId: string) => {
22
- const user = userList.find((u) => u.id === userId)
23
-
24
- if (!userId || !user) {
25
- return toast.push({
26
- status: 'error',
27
- title: 'Could not find User',
28
- })
29
- }
30
-
31
- return 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: `Added ${user.displayName} to assignees`,
39
- status: 'success',
40
- })
41
- })
42
- .catch((err) => {
43
- console.error(err)
44
-
45
- return toast.push({
46
- title: `Failed to add assignee`,
47
- description: userId,
48
- status: 'error',
49
- })
50
- })
51
- },
52
- [documentId, client, toast, userList]
53
- )
54
-
55
- const removeAssignee = React.useCallback(
56
- (userId: string) => {
57
- const user = userList.find((u) => u.id === userId)
58
-
59
- if (!userId || !user) {
60
- return toast.push({
61
- status: 'error',
62
- title: 'Could not find User',
63
- })
64
- }
65
-
66
- return client
67
- .patch(`workflow-metadata.${documentId}`)
68
- .unset([`assignees[@ == "${userId}"]`])
69
- .commit()
70
- .then(() => {
71
- return toast.push({
72
- title: `Removed ${user.displayName} from assignees`,
73
- status: 'success',
74
- })
75
- })
76
- .catch((err) => {
77
- console.error(err)
78
-
79
- return toast.push({
80
- title: `Failed to remove assignee`,
81
- description: documentId,
82
- status: 'error',
83
- })
84
- })
85
- },
86
- [client, toast, documentId, userList]
87
- )
88
-
89
- const clearAssignees = React.useCallback(() => {
90
- return client
91
- .patch(`workflow-metadata.${documentId}`)
92
- .unset([`assignees`])
93
- .commit()
94
- .then(() => {
95
- return toast.push({
96
- title: `Cleared assignees`,
97
- status: 'success',
98
- })
99
- })
100
- .catch((err) => {
101
- console.error(err)
102
-
103
- return toast.push({
104
- title: `Failed to clear assignees`,
105
- description: documentId,
106
- status: 'error',
107
- })
108
- })
109
- }, [client, toast, documentId])
110
-
111
- return (
112
- <UserSelectMenu
113
- style={{maxHeight: 300}}
114
- value={assignees || []}
115
- userList={userList}
116
- onAdd={addAssignee}
117
- onClear={clearAssignees}
118
- onRemove={removeAssignee}
119
- />
120
- )
121
- }
@@ -1,27 +0,0 @@
1
- import {Card} from '@sanity/ui'
2
- import {FunctionComponent} from 'react'
3
- import {ArraySchemaType, ArrayOfPrimitivesInputProps, useFormValue} from 'sanity'
4
- import {useProjectUsers} from 'sanity-plugin-utils'
5
-
6
- import {API_VERSION} from '../constants'
7
- import UserAssignment from './UserAssignment'
8
-
9
- const UserAssignmentInput: FunctionComponent<
10
- ArrayOfPrimitivesInputProps<string | number | boolean, ArraySchemaType>
11
- > = (props) => {
12
- const documentId = useFormValue([`documentId`])
13
- const userList = useProjectUsers({apiVersion: API_VERSION})
14
-
15
- const stringValue =
16
- Array.isArray(props?.value) && props?.value?.length
17
- ? props.value.map((item) => String(item))
18
- : []
19
-
20
- return (
21
- <Card border padding={1}>
22
- <UserAssignment userList={userList} assignees={stringValue} documentId={String(documentId)} />
23
- </Card>
24
- )
25
- }
26
-
27
- export default UserAssignmentInput
@@ -1,57 +0,0 @@
1
- import React from 'react'
2
- import {Button, Grid, Popover, useClickOutside} from '@sanity/ui'
3
- import {AddIcon} from '@sanity/icons'
4
-
5
- import AvatarGroup from './DocumentCard/AvatarGroup'
6
- import {User} from '../types'
7
- import UserAssignment from './UserAssignment'
8
-
9
- type UserDisplayProps = {
10
- userList: User[]
11
- assignees: string[]
12
- documentId: string
13
- disabled?: boolean
14
- }
15
-
16
- export default function UserDisplay(props: UserDisplayProps) {
17
- const {assignees, userList, documentId, disabled = false} = props
18
-
19
- const [button] = React.useState(null)
20
- const [popover, setPopover] = React.useState(null)
21
- const [isOpen, setIsOpen] = React.useState(false)
22
-
23
- const close = React.useCallback(() => setIsOpen(false), [])
24
- const open = React.useCallback(() => setIsOpen(true), [])
25
-
26
- useClickOutside(close, [button, popover])
27
-
28
- return (
29
- <Popover
30
- // @ts-ignore
31
- ref={setPopover}
32
- content={<UserAssignment userList={userList} assignees={assignees} documentId={documentId} />}
33
- portal
34
- open={isOpen}
35
- >
36
- {!assignees || assignees.length === 0 ? (
37
- <Button
38
- onClick={open}
39
- fontSize={1}
40
- padding={2}
41
- tabIndex={-1}
42
- icon={AddIcon}
43
- text="Assign"
44
- tone="positive"
45
- mode="ghost"
46
- disabled={disabled}
47
- />
48
- ) : (
49
- <Grid>
50
- <Button onClick={open} padding={0} mode="bleed" disabled={disabled}>
51
- <AvatarGroup users={userList.filter((u) => assignees.includes(u.id))} />
52
- </Button>
53
- </Grid>
54
- )}
55
- </Popover>
56
- )
57
- }
@@ -1,297 +0,0 @@
1
- import {Button, useToast} from '@sanity/ui'
2
- import {LexoRank} from 'lexorank'
3
- import React from 'react'
4
- import {useClient} from 'sanity'
5
- import {UserExtended} from 'sanity-plugin-utils'
6
-
7
- import {API_VERSION} from '../constants'
8
- import {generateMultipleOrderRanks} from '../helpers/generateMultipleOrderRanks'
9
- import {SanityDocumentWithMetadata, State} from '../types'
10
- import FloatingCard from './FloatingCard'
11
-
12
- type VerifyProps = {
13
- data: SanityDocumentWithMetadata[]
14
- userList: UserExtended[]
15
- states: State[]
16
- }
17
-
18
- // This component checks the validity of the data in the Kanban
19
- // It will only render something it there is invalid date
20
- // And will render buttons to fix the data
21
- export default function Verify(props: VerifyProps) {
22
- const {data, userList, states} = props
23
- const client = useClient({apiVersion: API_VERSION})
24
- const toast = useToast()
25
-
26
- // A lot of error-checking
27
- const documentsWithoutValidMetadataIds = data?.length
28
- ? data.reduce((acc, cur) => {
29
- const {documentId, state} = cur._metadata ?? {}
30
- const stateExists = states.find((s) => s.id === state)
31
-
32
- return !stateExists && documentId ? [...acc, documentId] : acc
33
- }, [] as string[])
34
- : []
35
-
36
- const documentsWithInvalidUserIds =
37
- data?.length && userList?.length
38
- ? data.reduce((acc, cur) => {
39
- const {documentId, assignees} = cur._metadata ?? {}
40
- const allAssigneesExist = assignees?.length
41
- ? assignees?.every((a) => userList.find((u) => u.id === a))
42
- : true
43
-
44
- return !allAssigneesExist && documentId ? [...acc, documentId] : acc
45
- }, [] as string[])
46
- : []
47
-
48
- const documentsWithoutOrderIds = data?.length
49
- ? data.reduce((acc, cur) => {
50
- const {documentId, orderRank} = cur._metadata ?? {}
51
-
52
- return !orderRank && documentId ? [...acc, documentId] : acc
53
- }, [] as string[])
54
- : []
55
-
56
- const documentsWithDuplicatedOrderIds = data?.length
57
- ? data.reduce((acc, cur) => {
58
- const {documentId, orderRank} = cur._metadata ?? {}
59
-
60
- return orderRank &&
61
- data.filter((d) => d._metadata?.orderRank === orderRank).length > 1 &&
62
- documentId
63
- ? [...acc, documentId]
64
- : acc
65
- }, [] as string[])
66
- : []
67
-
68
- // Updates metadata documents to a valid, existing state
69
- const correctDocuments = React.useCallback(
70
- async (ids: string[]) => {
71
- toast.push({
72
- title: 'Correcting...',
73
- status: 'info',
74
- })
75
-
76
- const tx = ids.reduce((item, documentId) => {
77
- return item.patch(`workflow-metadata.${documentId}`, {
78
- set: {state: states[0].id},
79
- })
80
- }, client.transaction())
81
-
82
- await tx.commit()
83
-
84
- toast.push({
85
- title: `Corrected ${
86
- ids.length === 1 ? `1 Document` : `${ids.length} Documents`
87
- }`,
88
- status: 'success',
89
- })
90
- },
91
- [client, states, toast]
92
- )
93
-
94
- // Remove users that are no longer in the project from documents
95
- const removeUsersFromDocuments = React.useCallback(
96
- async (ids: string[]) => {
97
- toast.push({
98
- title: 'Removing users...',
99
- status: 'info',
100
- })
101
-
102
- const tx = ids.reduce((item, documentId) => {
103
- const {assignees} =
104
- data.find((d) => d._id === documentId)?._metadata ?? {}
105
- const validAssignees = assignees?.length
106
- ? // eslint-disable-next-line max-nested-callbacks
107
- assignees.filter((a) => userList.find((u) => u.id === a)?.id)
108
- : []
109
-
110
- return item.patch(`workflow-metadata.${documentId}`, {
111
- set: {assignees: validAssignees},
112
- })
113
- }, client.transaction())
114
-
115
- await tx.commit()
116
-
117
- toast.push({
118
- title: `Corrected ${
119
- ids.length === 1 ? `1 Document` : `${ids.length} Documents`
120
- }`,
121
- status: 'success',
122
- })
123
- },
124
- [client, data, toast, userList]
125
- )
126
-
127
- // Add order value to metadata documents
128
- const addOrderToDocuments = React.useCallback(
129
- async (ids: string[]) => {
130
- toast.push({
131
- title: 'Adding ordering...',
132
- status: 'info',
133
- })
134
-
135
- // Get first and second order values, if they exist
136
- const [firstOrder, secondOrder] = [...data]
137
- .slice(0, 2)
138
- .map((d) => d._metadata?.orderRank)
139
- const minLexo = firstOrder ? LexoRank.parse(firstOrder) : undefined
140
- const maxLexo = secondOrder ? LexoRank.parse(secondOrder) : undefined
141
- const ranks = generateMultipleOrderRanks(ids.length, minLexo, maxLexo)
142
-
143
- const tx = client.transaction()
144
-
145
- // Create a new in-between value for each document
146
- for (let index = 0; index < ids.length; index += 1) {
147
- tx.patch(`workflow-metadata.${ids[index]}`, {
148
- set: {orderRank: ranks[index].toString()},
149
- })
150
- }
151
-
152
- await tx.commit()
153
-
154
- toast.push({
155
- title: `Added order to ${
156
- ids.length === 1 ? `1 Document` : `${ids.length} Documents`
157
- }`,
158
- status: 'success',
159
- })
160
- },
161
- [data, client, toast]
162
- )
163
-
164
- // Reset order value on all metadata documents
165
- const resetOrderOfAllDocuments = React.useCallback(
166
- async (ids: string[]) => {
167
- toast.push({
168
- title: 'Adding ordering...',
169
- status: 'info',
170
- })
171
-
172
- const ranks = generateMultipleOrderRanks(ids.length)
173
-
174
- const tx = client.transaction()
175
-
176
- // Create a new in-between value for each document
177
- for (let index = 0; index < ids.length; index += 1) {
178
- tx.patch(`workflow-metadata.${ids[index]}`, {
179
- set: {orderRank: ranks[index].toString()},
180
- })
181
- }
182
-
183
- await tx.commit()
184
-
185
- toast.push({
186
- title: `Added order to ${
187
- ids.length === 1 ? `1 Document` : `${ids.length} Documents`
188
- }`,
189
- status: 'success',
190
- })
191
- },
192
- [data, client, toast]
193
- )
194
-
195
- // A document could be deleted and the workflow metadata left behind
196
- const orphanedMetadataDocumentIds = React.useMemo(() => {
197
- return data.length
198
- ? data.filter((doc) => !doc?._id).map((doc) => doc._metadata.documentId)
199
- : []
200
- }, [data])
201
-
202
- const handleOrphans = React.useCallback(() => {
203
- toast.push({
204
- title: 'Removing orphaned metadata...',
205
- status: 'info',
206
- })
207
-
208
- const tx = client.transaction()
209
- orphanedMetadataDocumentIds.forEach((id) => {
210
- tx.delete(`workflow-metadata.${id}`)
211
- })
212
-
213
- tx.commit()
214
-
215
- toast.push({
216
- title: `Removed ${orphanedMetadataDocumentIds.length} orphaned metadata documents`,
217
- status: 'success',
218
- })
219
- }, [client, orphanedMetadataDocumentIds, toast])
220
-
221
- return (
222
- <FloatingCard>
223
- {documentsWithoutValidMetadataIds.length > 0 ? (
224
- <Button
225
- tone="caution"
226
- mode="ghost"
227
- onClick={() => correctDocuments(documentsWithoutValidMetadataIds)}
228
- text={
229
- documentsWithoutValidMetadataIds.length === 1
230
- ? `Correct 1 Document State`
231
- : `Correct ${documentsWithoutValidMetadataIds.length} Document States`
232
- }
233
- />
234
- ) : null}
235
- {documentsWithInvalidUserIds.length > 0 ? (
236
- <Button
237
- tone="caution"
238
- mode="ghost"
239
- onClick={() => removeUsersFromDocuments(documentsWithInvalidUserIds)}
240
- text={
241
- documentsWithInvalidUserIds.length === 1
242
- ? `Remove Invalid Users from 1 Document`
243
- : `Remove Invalid Users from ${documentsWithInvalidUserIds.length} Documents`
244
- }
245
- />
246
- ) : null}
247
- {documentsWithoutOrderIds.length > 0 ? (
248
- <Button
249
- tone="caution"
250
- mode="ghost"
251
- onClick={() => addOrderToDocuments(documentsWithoutOrderIds)}
252
- text={
253
- documentsWithoutOrderIds.length === 1
254
- ? `Set Order for 1 Document`
255
- : `Set Order for ${documentsWithoutOrderIds.length} Documents`
256
- }
257
- />
258
- ) : null}
259
- {documentsWithDuplicatedOrderIds.length > 0 ? (
260
- <>
261
- <Button
262
- tone="caution"
263
- mode="ghost"
264
- onClick={() => addOrderToDocuments(documentsWithDuplicatedOrderIds)}
265
- text={
266
- documentsWithDuplicatedOrderIds.length === 1
267
- ? `Set Unique Order for 1 Document`
268
- : `Set Unique Order for ${documentsWithDuplicatedOrderIds.length} Documents`
269
- }
270
- />
271
- <Button
272
- tone="caution"
273
- mode="ghost"
274
- onClick={() =>
275
- resetOrderOfAllDocuments(
276
- data.map((doc) => String(doc._metadata?.documentId))
277
- )
278
- }
279
- text={
280
- data.length === 1
281
- ? `Reset Order for 1 Document`
282
- : `Reset Order for all ${data.length} Documents`
283
- }
284
- />
285
- </>
286
- ) : null}
287
- {orphanedMetadataDocumentIds.length > 0 ? (
288
- <Button
289
- text="Cleanup orphaned metadata"
290
- onClick={handleOrphans}
291
- tone="caution"
292
- mode="ghost"
293
- />
294
- ) : null}
295
- </FloatingCard>
296
- )
297
- }
@@ -1,71 +0,0 @@
1
- import {useCallback, useContext, useState} from 'react'
2
- import {createContext} from 'react'
3
- import {LayoutProps} from 'sanity'
4
-
5
- import {DEFAULT_CONFIG} from '../constants'
6
- import {useWorkflowMetadata} from '../hooks/useWorkflowMetadata'
7
- import {KeyedMetadata, WorkflowConfig} from '../types'
8
-
9
- export type WorkflowContextValue = Required<WorkflowConfig> & {
10
- data: KeyedMetadata
11
- loading: boolean
12
- error: boolean | unknown | ProgressEvent
13
- ids: string[]
14
- addId: (id: string) => void
15
- removeId: (id: string) => void
16
- }
17
-
18
- const WorkflowContext = createContext<WorkflowContextValue>({
19
- data: {},
20
- loading: false,
21
- error: false,
22
- ids: [],
23
- addId: () => null,
24
- removeId: () => null,
25
- ...DEFAULT_CONFIG,
26
- })
27
-
28
- export function useWorkflowContext(id?: string) {
29
- const current = useContext(WorkflowContext)
30
-
31
- return {...current, metadata: id ? current.data[id] : null}
32
- }
33
-
34
- type WorkflowProviderProps = LayoutProps & {workflow: Required<WorkflowConfig>}
35
-
36
- /**
37
- * This Provider wraps the Studio and provides the workflow context to document actions and badges.
38
- * This is so individual actions and badges do not need to all register their own listeners.
39
- * Instead, each document "signals" its ID up to the provider, which then registers a single listener
40
- * This is performed inside of a component loaded at the root level of the Document Form
41
- */
42
- export function WorkflowProvider(props: WorkflowProviderProps) {
43
- const [ids, setIds] = useState<string[]>([])
44
- const addId = useCallback(
45
- (id: string) =>
46
- setIds((current) => (current.includes(id) ? current : [...current, id])),
47
- []
48
- )
49
- const removeId = useCallback(
50
- (id: string) => setIds((current) => current.filter((i) => i !== id)),
51
- []
52
- )
53
- const {data, loading, error} = useWorkflowMetadata(ids)
54
-
55
- return (
56
- <WorkflowContext.Provider
57
- value={{
58
- data,
59
- loading,
60
- error,
61
- ids,
62
- addId,
63
- removeId,
64
- states: props.workflow.states,
65
- schemaTypes: props.workflow.schemaTypes,
66
- }}
67
- >
68
- {props.renderDefault(props)}
69
- </WorkflowContext.Provider>
70
- )
71
- }
@@ -1,30 +0,0 @@
1
- import {useEffect} from 'react'
2
- import {ObjectInputProps} from 'sanity'
3
-
4
- import {useWorkflowContext} from './WorkflowContext'
5
-
6
- // This component is loaded at the root level of the Document Form
7
- // It is used to signal the document ID to the WorkflowProvider
8
- export default function WorkflowSignal(props: ObjectInputProps) {
9
- const documentId = props?.value?._id
10
- ? props.value._id.replace(`drafts.`, ``)
11
- : null
12
-
13
- const {addId, removeId} = useWorkflowContext()
14
-
15
- useEffect(() => {
16
- // On mount, add to the query of listening documents
17
- if (documentId) {
18
- addId(documentId)
19
- }
20
-
21
- // On unmount, remove from the query of listening documents
22
- return () => {
23
- if (documentId) {
24
- removeId(documentId)
25
- }
26
- }
27
- }, [documentId, addId, removeId])
28
-
29
- return props.renderDefault(props)
30
- }