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

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 (50) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +73 -12
  3. package/lib/{src/index.d.ts → index.d.ts} +3 -3
  4. package/lib/index.esm.js +1800 -1
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +1813 -1
  7. package/lib/index.js.map +1 -1
  8. package/package.json +51 -40
  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 +39 -0
  23. package/src/components/DocumentCard/core/TimeAgo.tsx +11 -0
  24. package/src/components/DocumentCard/index.tsx +156 -50
  25. package/src/components/DocumentList.tsx +122 -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 +73 -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/Validators.tsx +229 -0
  34. package/src/components/WorkflowTool.tsx +302 -163
  35. package/src/constants/index.ts +31 -0
  36. package/src/helpers/arraysContainMatchingString.ts +6 -0
  37. package/src/helpers/filterItemsAndSort.ts +41 -0
  38. package/src/helpers/initialRank.ts +13 -0
  39. package/src/hooks/useWorkflowDocuments.tsx +62 -70
  40. package/src/hooks/useWorkflowMetadata.tsx +0 -1
  41. package/src/index.ts +38 -58
  42. package/src/schema/workflow/workflow.metadata.ts +68 -0
  43. package/src/tools/index.ts +15 -0
  44. package/src/types/index.ts +27 -6
  45. package/src/actions/DemoteAction.tsx +0 -62
  46. package/src/actions/PromoteAction.tsx +0 -62
  47. package/src/components/Mutate.tsx +0 -54
  48. package/src/components/StateTimeline.tsx +0 -98
  49. package/src/components/UserSelectInput.tsx +0 -43
  50. package/src/schema/workflow/metadata.ts +0 -38
@@ -1,68 +1,43 @@
1
- import React from 'react'
2
- import {
3
- Flex,
4
- Card,
5
- Box,
6
- Grid,
7
- Spinner,
8
- Label,
9
- useToast,
10
- Container,
11
- useTheme,
12
- Button,
13
- } from '@sanity/ui'
14
- import {Feedback, useProjectUsers} from 'sanity-plugin-utils'
15
- import {Tool, useClient} from 'sanity'
16
1
  import {
17
2
  DragDropContext,
3
+ DragStart,
18
4
  Droppable,
19
- Draggable,
20
5
  DropResult,
21
- } from 'react-beautiful-dnd'
6
+ } from '@hello-pangea/dnd'
7
+ import {Box, Card, Container, Flex, Grid, Spinner, useTheme} from '@sanity/ui'
8
+ import {LexoRank} from 'lexorank'
9
+ import React from 'react'
10
+ import {Tool, useCurrentUser} from 'sanity'
11
+ import {Feedback, useProjectUsers} from 'sanity-plugin-utils'
22
12
 
23
- import {SanityDocumentWithMetadata, State} from '../types'
24
- import {DocumentCard} from './DocumentCard'
25
- import Mutate from './Mutate'
13
+ import {API_VERSION} from '../constants'
14
+ import {arraysContainMatchingString} from '../helpers/arraysContainMatchingString'
15
+ import {filterItemsAndSort} from '../helpers/filterItemsAndSort'
26
16
  import {useWorkflowDocuments} from '../hooks/useWorkflowDocuments'
27
-
28
- function filterItemsByState(
29
- items: SanityDocumentWithMetadata[],
30
- stateId: string
31
- ) {
32
- return items.filter((item) => item?._metadata?.state === stateId)
33
- }
34
-
35
- type WorkflowToolOptions = {
36
- schemaTypes: string[]
37
- states: State[]
38
- }
17
+ import {State, WorkflowConfig} from '../types'
18
+ import {DocumentCard} from './DocumentCard'
19
+ import DocumentList from './DocumentList'
20
+ import Filters from './Filters'
21
+ import StateTitle from './StateTitle'
22
+ import Validators from './Validators'
39
23
 
40
24
  type WorkflowToolProps = {
41
- tool: Tool<WorkflowToolOptions>
42
- }
43
-
44
- type MutateProps = {
45
- _id: string
46
- _type: string
47
- state: State
48
- documentId: string
25
+ tool: Tool<WorkflowConfig>
49
26
  }
50
27
 
51
28
  export default function WorkflowTool(props: WorkflowToolProps) {
52
29
  const {schemaTypes = [], states = []} = props?.tool?.options ?? {}
53
30
 
54
- const [mutatingDocs, setMutatingDocs] = React.useState<MutateProps[]>([])
55
- const mutationFinished = React.useCallback((documentId: string) => {
56
- setMutatingDocs((docs) => docs.filter((doc) => doc._id !== documentId))
57
- }, [])
58
-
59
- const client = useClient()
60
- const toast = useToast()
61
-
62
31
  const isDarkMode = useTheme().sanity.color.dark
63
32
  const defaultCardTone = isDarkMode ? 'default' : 'transparent'
64
33
 
65
- const userList = useProjectUsers() || []
34
+ const userList = useProjectUsers({apiVersion: API_VERSION})
35
+
36
+ const user = useCurrentUser()
37
+ const userRoleNames = user?.roles?.length
38
+ ? user?.roles.map((r) => r.name)
39
+ : []
40
+
66
41
  const {workflowData, operations} = useWorkflowDocuments(schemaTypes)
67
42
 
68
43
  // Data to display in cards
@@ -71,54 +46,182 @@ export default function WorkflowTool(props: WorkflowToolProps) {
71
46
  // Operations to perform on cards
72
47
  const {move} = operations
73
48
 
74
- const documentsWithoutMetadataIds = data
75
- .filter((doc) => !doc._metadata)
76
- .map((d) => d._id.replace(`drafts.`, ``))
77
-
78
- const importDocuments = React.useCallback(async (ids: string[]) => {
79
- toast.push({
80
- title: 'Importing documents',
81
- status: 'info',
82
- })
83
-
84
- const tx = ids.reduce((item, documentId) => {
85
- return item.createOrReplace({
86
- _id: `workflow-metadata.${documentId}`,
87
- _type: 'workflow.metadata',
88
- state: states[0].id,
89
- documentId,
90
- })
91
- }, client.transaction())
92
-
93
- await tx.commit()
94
-
95
- toast.push({
96
- title: 'Imported documents',
97
- status: 'success',
98
- })
99
- }, [])
49
+ const [undroppableStates, setUndroppableStates] = React.useState<string[]>([])
50
+ const [draggingFrom, setDraggingFrom] = React.useState(``)
51
+
52
+ // When drag starts, check for any States we should not allow dropping on
53
+ // Because of either:
54
+ // 1. The "destination" State requires user assignment and the user is not assigned to the dragged document
55
+ // 2. The "source" State State has a list of transitions and the "destination" State is not in that list
56
+ const handleDragStart = React.useCallback(
57
+ (start: DragStart) => {
58
+ const {draggableId, source} = start
59
+ const {droppableId: currentStateId} = source
60
+ setDraggingFrom(currentStateId)
61
+
62
+ const document = data.find(
63
+ (item) => item._metadata?.documentId === draggableId
64
+ )
65
+ const state = states.find((s) => s.id === currentStateId)
66
+
67
+ // This shouldn't happen but TypeScript
68
+ if (!document || !state) return
69
+
70
+ const undroppableStateIds = []
71
+ const statesThatRequireAssignmentIds = states
72
+ .filter((s) => s.requireAssignment)
73
+ .map((s) => s.id)
74
+
75
+ if (statesThatRequireAssignmentIds.length) {
76
+ const documentAssignees = document._metadata?.assignees ?? []
77
+ const userIsAssignedToDocument = user?.id
78
+ ? documentAssignees.includes(user.id)
79
+ : false
80
+
81
+ if (!userIsAssignedToDocument) {
82
+ undroppableStateIds.push(...statesThatRequireAssignmentIds)
83
+ }
84
+ }
85
+
86
+ const statesThatCannotBeTransitionedToIds =
87
+ state.transitions && state.transitions.length
88
+ ? states
89
+ .filter((s) => !state.transitions?.includes(s.id))
90
+ .map((s) => s.id)
91
+ : []
92
+
93
+ if (statesThatCannotBeTransitionedToIds.length) {
94
+ undroppableStateIds.push(...statesThatCannotBeTransitionedToIds)
95
+ }
96
+
97
+ // Remove currentStateId from undroppableStates
98
+ const undroppableExceptSelf = undroppableStateIds.filter(
99
+ (id) => id !== currentStateId
100
+ )
101
+
102
+ if (undroppableExceptSelf.length) {
103
+ setUndroppableStates(undroppableExceptSelf)
104
+ }
105
+ },
106
+ [data, states, user]
107
+ )
100
108
 
101
109
  const handleDragEnd = React.useCallback(
102
110
  (result: DropResult) => {
111
+ // Reset undroppable states
112
+ setUndroppableStates([])
113
+ setDraggingFrom(``)
114
+
103
115
  const {draggableId, source, destination} = result
104
- console.log(
105
- `sending ${draggableId} from ${source.droppableId} to ${destination?.droppableId}`
106
- )
107
116
 
108
- if (!destination || destination.droppableId === source.droppableId) {
117
+ if (
118
+ // No destination?
119
+ !destination ||
120
+ // No change in position?
121
+ (destination.droppableId === source.droppableId &&
122
+ destination.index === source.index)
123
+ ) {
109
124
  return
110
125
  }
111
126
 
112
- // The list of mutating docs is how we un/publish documents
113
- const mutatingDoc = move(draggableId, destination, states)
127
+ // Find all items in current state
128
+ const destinationStateItems = [
129
+ ...filterItemsAndSort(data, destination.droppableId, [], null),
130
+ ]
131
+
132
+ let newOrder
114
133
 
115
- if (mutatingDoc) {
116
- // @ts-ignore
117
- // @todo not sure if these types should be updated. will documentId every be undefined here?
118
- setMutatingDocs((current) => [...current, mutatingDoc])
134
+ if (!destinationStateItems.length) {
135
+ // Only item in state
136
+ // New minimum rank
137
+ newOrder = LexoRank.min().toString()
138
+ } else if (destination.index === 0) {
139
+ // Now first item in order
140
+ const firstItemOrderRank = [...destinationStateItems].shift()?._metadata
141
+ ?.orderRank
142
+ newOrder =
143
+ firstItemOrderRank && typeof firstItemOrderRank === 'string'
144
+ ? LexoRank.parse(firstItemOrderRank).genPrev().toString()
145
+ : LexoRank.min().toString()
146
+ } else if (destination.index + 1 === destinationStateItems.length) {
147
+ // Now last item in order
148
+ const lastItemOrderRank = [...destinationStateItems].pop()?._metadata
149
+ ?.orderRank
150
+ newOrder =
151
+ lastItemOrderRank && typeof lastItemOrderRank === 'string'
152
+ ? LexoRank.parse(lastItemOrderRank).genNext().toString()
153
+ : LexoRank.min().toString()
154
+ } else {
155
+ // Must be between two items
156
+ const itemBefore = destinationStateItems[destination.index - 1]
157
+ const itemBeforeRank = itemBefore?._metadata?.orderRank
158
+ const itemBeforeRankParsed = itemBefore._metadata.orderRank
159
+ ? LexoRank.parse(itemBeforeRank)
160
+ : LexoRank.min()
161
+ const itemAfter = destinationStateItems[destination.index]
162
+ const itemAfterRank = itemAfter?._metadata?.orderRank
163
+ const itemAfterRankParsed = itemAfter._metadata.orderRank
164
+ ? LexoRank.parse(itemAfterRank)
165
+ : LexoRank.max()
166
+
167
+ newOrder = itemBeforeRankParsed.between(itemAfterRankParsed).toString()
119
168
  }
169
+
170
+ move(draggableId, destination, states, newOrder)
120
171
  },
121
- [move, states]
172
+ [data, move, states]
173
+ )
174
+
175
+ // Used for the user filter UI
176
+ const uniqueAssignedUsers = React.useMemo(() => {
177
+ const uniqueUserIds = data.reduce((acc, item) => {
178
+ const {assignees = []} = item._metadata ?? {}
179
+ const newAssignees = assignees?.length
180
+ ? assignees.filter((a) => !acc.includes(a))
181
+ : []
182
+ return newAssignees.length ? [...acc, ...newAssignees] : acc
183
+ }, [] as string[])
184
+
185
+ return userList.filter((u) => uniqueUserIds.includes(u.id))
186
+ }, [data, userList])
187
+
188
+ // Selected user IDs filter the visible workflow documents
189
+ const [selectedUserIds, setSelectedUserIds] = React.useState<string[]>(
190
+ uniqueAssignedUsers.map((u) => u.id)
191
+ )
192
+ const toggleSelectedUser = React.useCallback((userId: string) => {
193
+ setSelectedUserIds((prev) =>
194
+ prev.includes(userId)
195
+ ? prev.filter((u) => u !== userId)
196
+ : [...prev, userId]
197
+ )
198
+ }, [])
199
+ const resetSelectedUsers = React.useCallback(() => {
200
+ setSelectedUserIds([])
201
+ }, [])
202
+
203
+ // Selected schema types filter the visible workflow documents
204
+ const [selectedSchemaTypes, setSelectedSchemaTypes] =
205
+ React.useState<string[]>(schemaTypes)
206
+ const toggleSelectedSchemaType = React.useCallback((schemaType: string) => {
207
+ setSelectedSchemaTypes((prev) =>
208
+ prev.includes(schemaType)
209
+ ? prev.filter((u) => u !== schemaType)
210
+ : [...prev, schemaType]
211
+ )
212
+ }, [])
213
+
214
+ // Document IDs that have validation errors
215
+ const [invalidDocumentIds, setInvalidDocumentIds] = React.useState<string[]>(
216
+ []
217
+ )
218
+ const toggleInvalidDocumentId = React.useCallback(
219
+ (docId: string, action: 'ADD' | 'REMOVE') => {
220
+ setInvalidDocumentIds((prev) =>
221
+ action === 'ADD' ? [...prev, docId] : prev.filter((id) => id !== docId)
222
+ )
223
+ },
224
+ []
122
225
  )
123
226
 
124
227
  if (!states?.length) {
@@ -133,96 +236,132 @@ export default function WorkflowTool(props: WorkflowToolProps) {
133
236
  )
134
237
  }
135
238
 
136
- if (error) {
239
+ if (error && !data.length) {
137
240
  return (
138
241
  <Container width={1} padding={5}>
139
- <Feedback tone="critical" title="Error with query" />
242
+ <Feedback
243
+ tone="critical"
244
+ title="Error querying for Workflow documents"
245
+ />
140
246
  </Container>
141
247
  )
142
248
  }
143
249
 
144
250
  return (
145
- <>
146
- {mutatingDocs.length ? (
147
- <div style={{position: `absolute`, bottom: 0, background: 'red'}}>
148
- {mutatingDocs.map((mutate) => (
149
- <Mutate
150
- key={mutate._id}
151
- {...mutate}
152
- onComplete={mutationFinished}
153
- />
154
- ))}
155
- </div>
156
- ) : null}
157
- {documentsWithoutMetadataIds.length > 0 && (
158
- <Box padding={5}>
159
- <Card border padding={3} tone="caution">
160
- <Flex align="center" justify="center">
161
- <Button
162
- onClick={() => importDocuments(documentsWithoutMetadataIds)}
163
- >
164
- Import {documentsWithoutMetadataIds.length} Missing{' '}
165
- {documentsWithoutMetadataIds.length === 1
166
- ? `Document`
167
- : `Documents`}{' '}
168
- into Workflow
169
- </Button>
170
- </Flex>
171
- </Card>
172
- </Box>
173
- )}
174
- <DragDropContext onDragEnd={handleDragEnd}>
251
+ <Flex direction="column" height="fill" overflow="hidden">
252
+ <Validators data={data} userList={userList} states={states} />
253
+ <Filters
254
+ uniqueAssignedUsers={uniqueAssignedUsers}
255
+ selectedUserIds={selectedUserIds}
256
+ toggleSelectedUser={toggleSelectedUser}
257
+ resetSelectedUsers={resetSelectedUsers}
258
+ schemaTypes={schemaTypes}
259
+ selectedSchemaTypes={selectedSchemaTypes}
260
+ toggleSelectedSchemaType={toggleSelectedSchemaType}
261
+ />
262
+ <DragDropContext onDragStart={handleDragStart} onDragEnd={handleDragEnd}>
175
263
  <Grid columns={states.length} height="fill">
176
- {states.map((state: State, stateIndex: number) => (
177
- <Card key={state.id} borderLeft={stateIndex > 0}>
178
- <Card paddingY={4} padding={3} style={{pointerEvents: `none`}}>
179
- <Label>{state.title}</Label>
180
- </Card>
181
- <Droppable droppableId={state.id}>
182
- {(provided, snapshot) => (
183
- <Card
184
- ref={provided.innerRef}
185
- tone={snapshot.isDraggingOver ? `primary` : defaultCardTone}
186
- height="fill"
187
- >
188
- {loading ? (
189
- <Flex padding={5} align="center" justify="center">
190
- <Spinner muted />
191
- </Flex>
192
- ) : null}
193
-
194
- {data.length > 0 &&
195
- filterItemsByState(data, state.id).map(
196
- (item, itemIndex) => (
197
- // The metadata's documentId is always the published one
198
- <Draggable
199
- key={item?._metadata?.documentId as string}
200
- draggableId={item?._metadata?.documentId as string}
201
- index={itemIndex}
264
+ {states.map((state: State, stateIndex: number) => {
265
+ const userRoleCanDrop = state?.roles?.length
266
+ ? arraysContainMatchingString(state.roles, userRoleNames)
267
+ : true
268
+ const isDropDisabled =
269
+ !userRoleCanDrop || undroppableStates.includes(state.id)
270
+
271
+ return (
272
+ <Card
273
+ key={state.id}
274
+ borderLeft={stateIndex > 0}
275
+ tone={defaultCardTone}
276
+ >
277
+ <Flex direction="column" height="fill">
278
+ <StateTitle
279
+ state={state}
280
+ requireAssignment={state.requireAssignment ?? false}
281
+ userRoleCanDrop={userRoleCanDrop}
282
+ // operation={state.operation}
283
+ isDropDisabled={isDropDisabled}
284
+ draggingFrom={draggingFrom}
285
+ />
286
+ <Box flex={1}>
287
+ <Droppable
288
+ droppableId={state.id}
289
+ isDropDisabled={isDropDisabled}
290
+ // props required for virtualization
291
+ mode="virtual"
292
+ renderClone={(provided, snapshot, rubric) => {
293
+ const item = data.find(
294
+ (doc) =>
295
+ doc?._metadata?.documentId === rubric.draggableId
296
+ )
297
+
298
+ return (
299
+ <div
300
+ {...provided.draggableProps}
301
+ {...provided.dragHandleProps}
302
+ ref={provided.innerRef}
202
303
  >
203
- {(draggableProvided, draggableSnapshot) => (
204
- <div
205
- ref={draggableProvided.innerRef}
206
- {...draggableProvided.draggableProps}
207
- {...draggableProvided.dragHandleProps}
208
- >
209
- <DocumentCard
210
- isDragging={draggableSnapshot.isDragging}
211
- item={item}
212
- userList={userList}
213
- />
214
- </div>
304
+ {item ? (
305
+ <DocumentCard
306
+ isDragDisabled={false}
307
+ userRoleCanDrop={userRoleCanDrop}
308
+ isDragging={snapshot.isDragging}
309
+ item={item}
310
+ states={states}
311
+ toggleInvalidDocumentId={
312
+ toggleInvalidDocumentId
313
+ }
314
+ userList={userList}
315
+ />
316
+ ) : (
317
+ <Feedback title="Item not found" tone="caution" />
215
318
  )}
216
- </Draggable>
319
+ </div>
217
320
  )
321
+ }}
322
+ >
323
+ {(provided, snapshot) => (
324
+ <Card
325
+ ref={provided.innerRef}
326
+ tone={
327
+ snapshot.isDraggingOver
328
+ ? `primary`
329
+ : defaultCardTone
330
+ }
331
+ height="fill"
332
+ paddingTop={1}
333
+ >
334
+ {loading ? (
335
+ <Flex padding={5} align="center" justify="center">
336
+ <Spinner muted />
337
+ </Flex>
338
+ ) : null}
339
+
340
+ <DocumentList
341
+ data={data}
342
+ invalidDocumentIds={invalidDocumentIds}
343
+ selectedSchemaTypes={selectedSchemaTypes}
344
+ selectedUserIds={selectedUserIds}
345
+ state={state}
346
+ states={states}
347
+ toggleInvalidDocumentId={toggleInvalidDocumentId}
348
+ user={user}
349
+ userList={userList}
350
+ userRoleCanDrop={userRoleCanDrop}
351
+ />
352
+
353
+ {/* Not required for virtualized lists */}
354
+ {/* {provided.placeholder} */}
355
+ </Card>
218
356
  )}
219
- </Card>
220
- )}
221
- </Droppable>
222
- </Card>
223
- ))}
357
+ </Droppable>
358
+ </Box>
359
+ </Flex>
360
+ </Card>
361
+ )
362
+ })}
224
363
  </Grid>
225
364
  </DragDropContext>
226
- </>
365
+ </Flex>
227
366
  )
228
367
  }
@@ -0,0 +1,31 @@
1
+ import {defineStates, WorkflowConfig} from '../types'
2
+
3
+ export const API_VERSION = `2023-01-01`
4
+
5
+ export const DEFAULT_CONFIG: WorkflowConfig = {
6
+ schemaTypes: [],
7
+ states: defineStates([
8
+ {
9
+ id: 'inReview',
10
+ title: 'In review',
11
+ color: 'primary',
12
+ roles: ['editor', 'administrator'],
13
+ transitions: ['changesRequested', 'approved'],
14
+ },
15
+ {
16
+ id: 'changesRequested',
17
+ title: 'Changes requested',
18
+ color: 'warning',
19
+ roles: ['editor', 'administrator'],
20
+ transitions: ['approved'],
21
+ },
22
+ {
23
+ id: 'approved',
24
+ title: 'Approved',
25
+ color: 'success',
26
+ roles: ['administrator'],
27
+ requireAssignment: true,
28
+ transitions: ['changesRequested'],
29
+ },
30
+ ]),
31
+ }
@@ -0,0 +1,6 @@
1
+ export function arraysContainMatchingString(
2
+ one: string[],
3
+ two: string[]
4
+ ): boolean {
5
+ return one.some((item) => two.includes(item))
6
+ }
@@ -0,0 +1,41 @@
1
+ import {SanityDocumentWithMetadata} from '../types'
2
+
3
+ export function filterItemsAndSort(
4
+ items: SanityDocumentWithMetadata[],
5
+ stateId: string,
6
+ selectedUsers: string[] = [],
7
+ selectedSchemaTypes: null | string[] = []
8
+ ): SanityDocumentWithMetadata[] {
9
+ return (
10
+ items
11
+ // Only items that have existing documents
12
+ .filter((item) => item?._id)
13
+ // Only items of this state
14
+ .filter((item) => item?._metadata?.state === stateId)
15
+ // Only items with selected users, if the document has any assigned users
16
+ .filter((item) =>
17
+ selectedUsers.length && item._metadata?.assignees?.length
18
+ ? item._metadata?.assignees.some((assignee) =>
19
+ selectedUsers.includes(assignee)
20
+ )
21
+ : !selectedUsers.length
22
+ )
23
+ // Only items of selected schema types, if any are selected
24
+ .filter((item) => {
25
+ if (!selectedSchemaTypes) {
26
+ return true
27
+ }
28
+
29
+ return selectedSchemaTypes.length
30
+ ? selectedSchemaTypes.includes(item._type)
31
+ : false
32
+ })
33
+ // Sort by metadata orderRank, a string field
34
+ .sort((a, b) => {
35
+ const aOrderRank = a._metadata?.orderRank || '0'
36
+ const bOrderRank = b._metadata?.orderRank || '0'
37
+
38
+ return aOrderRank.localeCompare(bOrderRank)
39
+ })
40
+ )
41
+ }
@@ -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
+ }