sanity-plugin-workflow 1.0.0-beta.1 → 1.0.0-beta.3

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 (49) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +71 -12
  3. package/lib/{src/index.d.ts → index.d.ts} +3 -3
  4. package/lib/index.esm.js +1691 -1
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +1704 -1
  7. package/lib/index.js.map +1 -1
  8. package/package.json +48 -38
  9. package/src/actions/AssignWorkflow.tsx +48 -0
  10. package/src/actions/BeginWorkflow.tsx +68 -0
  11. package/src/actions/CompleteWorkflow.tsx +41 -0
  12. package/src/actions/RequestReviewAction.js +1 -7
  13. package/src/actions/UpdateWorkflow.tsx +142 -0
  14. package/src/badges/AssigneesBadge.tsx +52 -0
  15. package/src/badges/{index.tsx → StateBadge.tsx} +4 -8
  16. package/src/components/DocumentCard/AvatarGroup.tsx +12 -8
  17. package/src/components/DocumentCard/CompleteButton.tsx +53 -0
  18. package/src/components/DocumentCard/EditButton.tsx +3 -2
  19. package/src/components/DocumentCard/Field.tsx +38 -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 +32 -0
  23. package/src/components/DocumentCard/core/TimeAgo.tsx +11 -0
  24. package/src/components/DocumentCard/index.tsx +156 -50
  25. package/src/components/Filters.tsx +168 -0
  26. package/src/components/FloatingCard.tsx +29 -0
  27. package/src/components/StateTitle/Status.tsx +27 -0
  28. package/src/components/StateTitle/index.tsx +73 -0
  29. package/src/components/UserAssignment.tsx +57 -75
  30. package/src/components/UserAssignmentInput.tsx +27 -0
  31. package/src/components/UserDisplay.tsx +57 -0
  32. package/src/components/Validators.tsx +196 -0
  33. package/src/components/WorkflowTool.tsx +301 -160
  34. package/src/constants/index.ts +31 -0
  35. package/src/helpers/arraysContainMatchingString.ts +6 -0
  36. package/src/helpers/filterItemsAndSort.ts +39 -0
  37. package/src/helpers/initialRank.ts +13 -0
  38. package/src/hooks/useWorkflowDocuments.tsx +62 -70
  39. package/src/hooks/useWorkflowMetadata.tsx +0 -1
  40. package/src/index.ts +38 -58
  41. package/src/schema/workflow/workflow.metadata.ts +68 -0
  42. package/src/tools/index.ts +15 -0
  43. package/src/types/index.ts +27 -6
  44. package/src/actions/DemoteAction.tsx +0 -62
  45. package/src/actions/PromoteAction.tsx +0 -62
  46. package/src/components/Mutate.tsx +0 -54
  47. package/src/components/StateTimeline.tsx +0 -98
  48. package/src/components/UserSelectInput.tsx +0 -43
  49. package/src/schema/workflow/metadata.ts +0 -38
@@ -1,11 +1,10 @@
1
1
  import React from 'react'
2
- import {Button, Popover, useToast} from '@sanity/ui'
3
- import {AddIcon} from '@sanity/icons'
2
+ import {useToast} from '@sanity/ui'
4
3
  import {UserSelectMenu} from 'sanity-plugin-utils'
5
4
  import {useClient} from 'sanity'
6
5
 
7
- import AvatarGroup from './DocumentCard/AvatarGroup'
8
6
  import {User} from '../types'
7
+ import {API_VERSION} from '../constants'
9
8
 
10
9
  type UserAssignmentProps = {
11
10
  userList: User[]
@@ -15,28 +14,28 @@ type UserAssignmentProps = {
15
14
 
16
15
  export default function UserAssignment(props: UserAssignmentProps) {
17
16
  const {assignees, userList, documentId} = props
18
- const client = useClient()
17
+ const client = useClient({apiVersion: API_VERSION})
19
18
  const toast = useToast()
20
- const [openId, setOpenId] = React.useState<string>(``)
21
19
 
22
20
  const addAssignee = React.useCallback(
23
21
  (userId: string) => {
24
- if (!userId) {
22
+ const user = userList.find((u) => u.id === userId)
23
+
24
+ if (!userId || !user) {
25
25
  return toast.push({
26
26
  status: 'error',
27
- title: 'No user selected',
27
+ title: 'Could not find User',
28
28
  })
29
29
  }
30
30
 
31
- client
31
+ return client
32
32
  .patch(`workflow-metadata.${documentId}`)
33
33
  .setIfMissing({assignees: []})
34
34
  .insert(`after`, `assignees[-1]`, [userId])
35
35
  .commit()
36
36
  .then(() => {
37
37
  return toast.push({
38
- title: `Assigned user to document`,
39
- description: userId,
38
+ title: `Added ${user.displayName} to assignees`,
40
39
  status: 'success',
41
40
  })
42
41
  })
@@ -50,90 +49,73 @@ export default function UserAssignment(props: UserAssignmentProps) {
50
49
  })
51
50
  })
52
51
  },
53
- [documentId, client, toast]
52
+ [documentId, client, toast, userList]
54
53
  )
55
54
 
56
55
  const removeAssignee = React.useCallback(
57
- (id: string, userId: string) => {
58
- client
59
- .patch(`workflow-metadata.${id}`)
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}`)
60
68
  .unset([`assignees[@ == "${userId}"]`])
61
69
  .commit()
62
- .then((res) => res)
70
+ .then(() => {
71
+ return toast.push({
72
+ title: `Removed ${user.displayName} from assignees`,
73
+ status: 'success',
74
+ })
75
+ })
63
76
  .catch((err) => {
64
77
  console.error(err)
65
78
 
66
79
  return toast.push({
67
80
  title: `Failed to remove assignee`,
68
- description: id,
81
+ description: documentId,
69
82
  status: 'error',
70
83
  })
71
84
  })
72
85
  },
73
- [client, toast]
86
+ [client, toast, documentId, userList]
74
87
  )
75
88
 
76
- const clearAssignees = React.useCallback(
77
- (id: string) => {
78
- client
79
- .patch(`workflow-metadata.${id}`)
80
- .unset([`assignees`])
81
- .commit()
82
- .then((res) => res)
83
- .catch((err) => {
84
- console.error(err)
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)
85
102
 
86
- return toast.push({
87
- title: `Failed to clear assignees`,
88
- description: id,
89
- status: 'error',
90
- })
103
+ return toast.push({
104
+ title: `Failed to clear assignees`,
105
+ description: documentId,
106
+ status: 'error',
91
107
  })
92
- },
93
- [client, toast]
94
- )
108
+ })
109
+ }, [client, toast, documentId])
95
110
 
96
111
  return (
97
- <Popover
98
- // @ts-ignore
99
- // ref={setPopoverRef}
100
- // onKeyDown={handleKeyDown}
101
- content={
102
- <UserSelectMenu
103
- style={{maxHeight: 300}}
104
- value={assignees || []}
105
- userList={userList}
106
- onAdd={addAssignee}
107
- onClear={clearAssignees}
108
- onRemove={removeAssignee}
109
- open={openId === documentId}
110
- />
111
- }
112
- portal
113
- open={openId === documentId}
114
- >
115
- {!assignees || assignees.length === 0 ? (
116
- <Button
117
- onClick={() => setOpenId(documentId)}
118
- fontSize={1}
119
- padding={2}
120
- tabIndex={-1}
121
- icon={AddIcon}
122
- text="Assign"
123
- tone="positive"
124
- />
125
- ) : (
126
- <Button
127
- onClick={() => setOpenId(documentId)}
128
- padding={0}
129
- mode="bleed"
130
- style={{width: `100%`}}
131
- >
132
- <AvatarGroup
133
- users={userList.filter((u) => assignees.includes(u.id))}
134
- />
135
- </Button>
136
- )}
137
- </Popover>
112
+ <UserSelectMenu
113
+ style={{maxHeight: 300}}
114
+ value={assignees || []}
115
+ userList={userList}
116
+ onAdd={addAssignee}
117
+ onClear={clearAssignees}
118
+ onRemove={removeAssignee}
119
+ />
138
120
  )
139
121
  }
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,57 @@
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
+ }
@@ -0,0 +1,196 @@
1
+ import React from 'react'
2
+ import {useToast, Button} from '@sanity/ui'
3
+ import {useClient} from 'sanity'
4
+ import {UserExtended} from 'sanity-plugin-utils'
5
+ import {LexoRank} from 'lexorank'
6
+
7
+ import FloatingCard from './FloatingCard'
8
+ import {API_VERSION} from '../constants'
9
+ import {SanityDocumentWithMetadata, State} from '../types'
10
+
11
+ type ValidatorsProps = {
12
+ data: SanityDocumentWithMetadata[]
13
+ userList: UserExtended[]
14
+ states: State[]
15
+ }
16
+
17
+ export default function Validators({data, userList, states}: ValidatorsProps) {
18
+ const client = useClient({apiVersion: API_VERSION})
19
+ const toast = useToast()
20
+
21
+ // A lot of error-checking
22
+ const documentsWithoutValidMetadataIds = data?.length
23
+ ? data.reduce((acc, cur) => {
24
+ const {documentId, state} = cur._metadata ?? {}
25
+ const stateExists = states.find((s) => s.id === state)
26
+
27
+ return !stateExists && documentId ? [...acc, documentId] : acc
28
+ }, [] as string[])
29
+ : []
30
+
31
+ const documentsWithInvalidUserIds = data?.length
32
+ ? data.reduce((acc, cur) => {
33
+ const {documentId, assignees} = cur._metadata ?? {}
34
+ const allAssigneesExist = assignees?.length
35
+ ? assignees?.every((a) => userList.find((u) => u.id === a))
36
+ : true
37
+
38
+ return !allAssigneesExist && documentId ? [...acc, documentId] : acc
39
+ }, [] as string[])
40
+ : []
41
+
42
+ const documentsWithoutOrderIds = data?.length
43
+ ? data.reduce((acc, cur) => {
44
+ const {documentId, orderRank} = cur._metadata ?? {}
45
+
46
+ return !orderRank && documentId ? [...acc, documentId] : acc
47
+ }, [] as string[])
48
+ : []
49
+
50
+ // Updates metadata documents to a valid, existing state
51
+ const correctDocuments = React.useCallback(
52
+ async (ids: string[]) => {
53
+ toast.push({
54
+ title: 'Correcting...',
55
+ status: 'info',
56
+ })
57
+
58
+ const tx = ids.reduce((item, documentId) => {
59
+ return item.patch(`workflow-metadata.${documentId}`, {
60
+ set: {state: states[0].id},
61
+ })
62
+ }, client.transaction())
63
+
64
+ await tx.commit()
65
+
66
+ toast.push({
67
+ title: `Corrected ${
68
+ ids.length === 1 ? `1 Document` : `${ids.length} Documents`
69
+ }`,
70
+ status: 'success',
71
+ })
72
+ },
73
+ [client, states, toast]
74
+ )
75
+
76
+ // Remove users that are no longer in the project from documents
77
+ const removeUsersFromDocuments = React.useCallback(
78
+ async (ids: string[]) => {
79
+ toast.push({
80
+ title: 'Removing users...',
81
+ status: 'info',
82
+ })
83
+
84
+ const tx = ids.reduce((item, documentId) => {
85
+ const {assignees} =
86
+ data.find((d) => d._id === documentId)?._metadata ?? {}
87
+ const validAssignees = assignees?.length
88
+ ? // eslint-disable-next-line max-nested-callbacks
89
+ assignees.filter((a) => userList.find((u) => u.id === a)?.id)
90
+ : []
91
+
92
+ return item.patch(`workflow-metadata.${documentId}`, {
93
+ set: {assignees: validAssignees},
94
+ })
95
+ }, client.transaction())
96
+
97
+ await tx.commit()
98
+
99
+ toast.push({
100
+ title: `Corrected ${
101
+ ids.length === 1 ? `1 Document` : `${ids.length} Documents`
102
+ }`,
103
+ status: 'success',
104
+ })
105
+ },
106
+ [client, data, toast, userList]
107
+ )
108
+
109
+ // Add order value to metadata documents
110
+ const addOrderToDocuments = React.useCallback(
111
+ async (ids: string[]) => {
112
+ toast.push({
113
+ title: 'Adding ordering...',
114
+ status: 'info',
115
+ })
116
+
117
+ // Get first order value
118
+ const firstOrder = data[0]?._metadata?.orderRank
119
+ let newLexo =
120
+ firstOrder && data.length !== ids.length
121
+ ? LexoRank.parse(firstOrder)
122
+ : LexoRank.min()
123
+
124
+ const tx = client.transaction()
125
+
126
+ for (let index = 0; index < ids.length; index += 1) {
127
+ newLexo = newLexo.genNext().genNext()
128
+
129
+ tx.patch(`workflow-metadata.${ids[index]}`, {
130
+ set: {orderRank: newLexo.toString()},
131
+ })
132
+ }
133
+
134
+ await tx.commit()
135
+
136
+ toast.push({
137
+ title: `Added order to ${
138
+ ids.length === 1 ? `1 Document` : `${ids.length} Documents`
139
+ }`,
140
+ status: 'success',
141
+ })
142
+ },
143
+ [data, client, toast]
144
+ )
145
+
146
+ return (
147
+ <FloatingCard>
148
+ {documentsWithoutValidMetadataIds.length > 0 ? (
149
+ <Button
150
+ tone="caution"
151
+ onClick={() => correctDocuments(documentsWithoutValidMetadataIds)}
152
+ text={
153
+ documentsWithoutValidMetadataIds.length === 1
154
+ ? `Correct 1 Document State`
155
+ : `Correct ${documentsWithoutValidMetadataIds.length} Document States`
156
+ }
157
+ />
158
+ ) : null}
159
+ {documentsWithInvalidUserIds.length > 0 ? (
160
+ <Button
161
+ tone="caution"
162
+ onClick={() => removeUsersFromDocuments(documentsWithInvalidUserIds)}
163
+ text={
164
+ documentsWithInvalidUserIds.length === 1
165
+ ? `Remove Invalid Users from 1 Document`
166
+ : `Remove Invalid Users from ${documentsWithInvalidUserIds.length} Documents`
167
+ }
168
+ />
169
+ ) : null}
170
+ {documentsWithoutOrderIds.length > 0 ? (
171
+ <Button
172
+ tone="caution"
173
+ onClick={() => addOrderToDocuments(documentsWithoutOrderIds)}
174
+ text={
175
+ documentsWithoutOrderIds.length === 1
176
+ ? `Set Order for 1 Document`
177
+ : `Set Order for ${documentsWithoutOrderIds.length} Documents`
178
+ }
179
+ />
180
+ ) : null}
181
+ {/* <Button
182
+ tone="caution"
183
+ onClick={() =>
184
+ addOrderToDocuments(
185
+ data.map((doc) => String(doc._metadata?.documentId))
186
+ )
187
+ }
188
+ text={
189
+ data.length === 1
190
+ ? `Reset Order for 1 Document`
191
+ : `Reset Order for all ${data.length} Documents`
192
+ }
193
+ /> */}
194
+ </FloatingCard>
195
+ )
196
+ }