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.
- package/LICENSE +1 -1
- package/README.md +81 -13
- package/lib/{src/index.d.ts → index.d.ts} +4 -3
- package/lib/index.esm.js +2106 -1
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +2119 -1
- package/lib/index.js.map +1 -1
- package/package.json +51 -40
- package/src/actions/AssignWorkflow.tsx +49 -0
- package/src/actions/BeginWorkflow.tsx +63 -0
- package/src/actions/CompleteWorkflow.tsx +41 -0
- package/src/actions/UpdateWorkflow.tsx +126 -0
- package/src/badges/AssigneesBadge.tsx +53 -0
- package/src/badges/StateBadge.tsx +28 -0
- package/src/components/DocumentCard/AvatarGroup.tsx +12 -8
- package/src/components/DocumentCard/CompleteButton.tsx +68 -0
- package/src/components/DocumentCard/EditButton.tsx +3 -2
- package/src/components/DocumentCard/Field.tsx +38 -0
- package/src/components/DocumentCard/Validate.tsx +21 -0
- package/src/components/DocumentCard/ValidationStatus.tsx +37 -0
- package/src/components/DocumentCard/core/DraftStatus.tsx +32 -0
- package/src/components/DocumentCard/core/PublishedStatus.tsx +39 -0
- package/src/components/DocumentCard/core/TimeAgo.tsx +11 -0
- package/src/components/DocumentCard/index.tsx +177 -68
- package/src/components/DocumentList.tsx +169 -0
- package/src/components/Filters.tsx +168 -0
- package/src/components/FloatingCard.tsx +29 -0
- package/src/components/StateTitle/Status.tsx +27 -0
- package/src/components/StateTitle/index.tsx +78 -0
- package/src/components/UserAssignment.tsx +57 -75
- package/src/components/UserAssignmentInput.tsx +27 -0
- package/src/components/UserDisplay.tsx +57 -0
- package/src/components/Verify.tsx +297 -0
- package/src/components/WorkflowContext.tsx +71 -0
- package/src/components/WorkflowSignal.tsx +30 -0
- package/src/components/WorkflowTool.tsx +373 -162
- package/src/constants/index.ts +31 -0
- package/src/helpers/arraysContainMatchingString.ts +6 -0
- package/src/helpers/filterItemsAndSort.ts +41 -0
- package/src/helpers/generateMultipleOrderRanks.ts +80 -0
- package/src/helpers/initialRank.ts +13 -0
- package/src/hooks/useWorkflowDocuments.tsx +76 -78
- package/src/hooks/useWorkflowMetadata.tsx +31 -26
- package/src/index.ts +60 -57
- package/src/schema/workflow/workflow.metadata.ts +68 -0
- package/src/tools/index.ts +15 -0
- package/src/types/index.ts +27 -6
- package/src/actions/DemoteAction.tsx +0 -62
- package/src/actions/PromoteAction.tsx +0 -62
- package/src/actions/RequestReviewAction.js +0 -61
- package/src/actions/index.js +0 -21
- package/src/badges/index.tsx +0 -31
- package/src/components/Mutate.tsx +0 -54
- package/src/components/StateTimeline.tsx +0 -98
- package/src/components/UserSelectInput.tsx +0 -43
- package/src/schema/workflow/metadata.ts +0 -38
|
@@ -1,92 +1,201 @@
|
|
|
1
1
|
/* eslint-disable react/prop-types */
|
|
2
|
+
import {DragHandleIcon} from '@sanity/icons'
|
|
3
|
+
import {Box, Card, CardTone, Flex, Stack, useTheme} from '@sanity/ui'
|
|
4
|
+
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
2
5
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Popover,
|
|
8
|
-
Stack,
|
|
9
|
-
useClickOutside,
|
|
10
|
-
useTheme,
|
|
11
|
-
} from '@sanity/ui'
|
|
12
|
-
import {AddIcon, DragHandleIcon} from '@sanity/icons'
|
|
13
|
-
import React, {useState} from 'react'
|
|
14
|
-
import {useSchema, SchemaType} from 'sanity'
|
|
15
|
-
import {UserSelectMenu} from 'sanity-plugin-utils'
|
|
6
|
+
SchemaType,
|
|
7
|
+
useSchema,
|
|
8
|
+
ValidationStatus as ValidationStatusType,
|
|
9
|
+
} from 'sanity'
|
|
16
10
|
import {Preview} from 'sanity'
|
|
17
11
|
|
|
12
|
+
import {SanityDocumentWithMetadata, State, User} from '../../types'
|
|
13
|
+
import UserDisplay from '../UserDisplay'
|
|
14
|
+
import CompleteButton from './CompleteButton'
|
|
15
|
+
import {DraftStatus} from './core/DraftStatus'
|
|
16
|
+
import {PublishedStatus} from './core/PublishedStatus'
|
|
18
17
|
import EditButton from './EditButton'
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import UserAssignment from '../UserAssignment'
|
|
18
|
+
import Validate from './Validate'
|
|
19
|
+
import {ValidationStatus} from './ValidationStatus'
|
|
22
20
|
|
|
23
21
|
type DocumentCardProps = {
|
|
24
|
-
|
|
22
|
+
isDragDisabled: boolean
|
|
23
|
+
isPatching: boolean
|
|
24
|
+
userRoleCanDrop: boolean
|
|
25
25
|
isDragging: boolean
|
|
26
26
|
item: SanityDocumentWithMetadata
|
|
27
|
+
states: State[]
|
|
28
|
+
toggleInvalidDocumentId: (
|
|
29
|
+
documentId: string,
|
|
30
|
+
action: 'ADD' | 'REMOVE'
|
|
31
|
+
) => void
|
|
32
|
+
userList: User[]
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export function DocumentCard(props: DocumentCardProps) {
|
|
30
|
-
const {
|
|
36
|
+
const {
|
|
37
|
+
isDragDisabled,
|
|
38
|
+
isPatching,
|
|
39
|
+
userRoleCanDrop,
|
|
40
|
+
isDragging,
|
|
41
|
+
item,
|
|
42
|
+
states,
|
|
43
|
+
toggleInvalidDocumentId,
|
|
44
|
+
userList,
|
|
45
|
+
} = props
|
|
31
46
|
const {assignees = [], documentId} = item._metadata ?? {}
|
|
32
47
|
const schema = useSchema()
|
|
48
|
+
const state = states.find((s) => s.id === item._metadata?.state)
|
|
33
49
|
|
|
34
50
|
const isDarkMode = useTheme().sanity.color.dark
|
|
35
|
-
const defaultCardTone = isDarkMode ?
|
|
51
|
+
const defaultCardTone = isDarkMode ? `transparent` : `default`
|
|
52
|
+
|
|
53
|
+
// Validation only runs if the state requests it
|
|
54
|
+
// Because it's not performant to run it on many documents simultaneously
|
|
55
|
+
// So we fake it here, and maybe set it inside <Validate />
|
|
56
|
+
const [optimisticValidation, setOptimisticValidation] =
|
|
57
|
+
useState<ValidationStatusType>({
|
|
58
|
+
isValidating: state?.requireValidation ?? false,
|
|
59
|
+
validation: [],
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const {isValidating, validation} = optimisticValidation
|
|
63
|
+
|
|
64
|
+
const handleValidation = useCallback((updates: ValidationStatusType) => {
|
|
65
|
+
setOptimisticValidation(updates)
|
|
66
|
+
}, [])
|
|
67
|
+
|
|
68
|
+
const cardTone = useMemo(() => {
|
|
69
|
+
let tone: CardTone = defaultCardTone
|
|
36
70
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
71
|
+
if (!userRoleCanDrop) return isDarkMode ? `default` : `transparent`
|
|
72
|
+
if (!documentId) return tone
|
|
73
|
+
if (isPatching) tone = isDarkMode ? `default` : `transparent`
|
|
74
|
+
if (isDragging) tone = `positive`
|
|
40
75
|
|
|
41
|
-
|
|
76
|
+
if (state?.requireValidation && !isValidating && validation.length > 0) {
|
|
77
|
+
if (validation.some((v) => v.level === 'error')) {
|
|
78
|
+
tone = `critical`
|
|
79
|
+
} else {
|
|
80
|
+
tone = `caution`
|
|
81
|
+
}
|
|
82
|
+
}
|
|
42
83
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
84
|
+
return tone
|
|
85
|
+
}, [
|
|
86
|
+
defaultCardTone,
|
|
87
|
+
userRoleCanDrop,
|
|
88
|
+
isPatching,
|
|
89
|
+
isDarkMode,
|
|
90
|
+
documentId,
|
|
91
|
+
isDragging,
|
|
92
|
+
isValidating,
|
|
93
|
+
validation,
|
|
94
|
+
state?.requireValidation,
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
// Update validation status
|
|
98
|
+
// Cannot be done in the above memo because it would set state during render
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!isValidating && validation.length > 0) {
|
|
101
|
+
if (validation.some((v) => v.level === 'error')) {
|
|
102
|
+
toggleInvalidDocumentId(documentId, 'ADD')
|
|
103
|
+
} else {
|
|
104
|
+
toggleInvalidDocumentId(documentId, 'REMOVE')
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
toggleInvalidDocumentId(documentId, 'REMOVE')
|
|
108
|
+
}
|
|
109
|
+
}, [documentId, isValidating, toggleInvalidDocumentId, validation])
|
|
110
|
+
|
|
111
|
+
const hasError = useMemo(
|
|
112
|
+
() => (isValidating ? false : validation.some((v) => v.level === 'error')),
|
|
113
|
+
[isValidating, validation]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
const isLastState = useMemo(
|
|
117
|
+
() => states[states.length - 1].id === item._metadata?.state,
|
|
118
|
+
[states, item._metadata.state]
|
|
119
|
+
)
|
|
48
120
|
|
|
49
121
|
return (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
122
|
+
<>
|
|
123
|
+
{state?.requireValidation ? (
|
|
124
|
+
<Validate
|
|
125
|
+
documentId={documentId}
|
|
126
|
+
type={item._type}
|
|
127
|
+
onChange={handleValidation}
|
|
128
|
+
/>
|
|
129
|
+
) : null}
|
|
130
|
+
<Box paddingBottom={3} paddingX={3}>
|
|
131
|
+
<Card radius={2} shadow={isDragging ? 3 : 1} tone={cardTone}>
|
|
132
|
+
<Stack>
|
|
133
|
+
<Card
|
|
134
|
+
borderBottom
|
|
135
|
+
radius={2}
|
|
136
|
+
padding={3}
|
|
137
|
+
paddingLeft={2}
|
|
138
|
+
tone={cardTone}
|
|
139
|
+
style={{pointerEvents: 'none'}}
|
|
140
|
+
>
|
|
141
|
+
<Flex align="center" justify="space-between" gap={1}>
|
|
142
|
+
<Box flex={1}>
|
|
143
|
+
<Preview
|
|
144
|
+
layout="default"
|
|
145
|
+
skipVisibilityCheck
|
|
146
|
+
value={item}
|
|
147
|
+
schemaType={schema.get(item._type) as SchemaType}
|
|
148
|
+
/>
|
|
149
|
+
</Box>
|
|
150
|
+
<Box style={{flexShrink: 0}}>
|
|
151
|
+
{hasError || isDragDisabled || isPatching ? null : (
|
|
152
|
+
<DragHandleIcon />
|
|
153
|
+
)}
|
|
154
|
+
</Box>
|
|
155
|
+
</Flex>
|
|
156
|
+
</Card>
|
|
157
|
+
|
|
158
|
+
<Card padding={2} radius={2} tone="inherit">
|
|
159
|
+
<Flex align="center" justify="space-between" gap={3}>
|
|
160
|
+
<Box flex={1}>
|
|
161
|
+
{documentId && (
|
|
162
|
+
<UserDisplay
|
|
163
|
+
userList={userList}
|
|
164
|
+
assignees={assignees}
|
|
165
|
+
documentId={documentId}
|
|
166
|
+
disabled={!userRoleCanDrop}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
169
|
+
</Box>
|
|
170
|
+
{validation.length > 0 ? (
|
|
171
|
+
<ValidationStatus validation={validation} />
|
|
172
|
+
) : null}
|
|
173
|
+
<DraftStatus document={item} />
|
|
174
|
+
<PublishedStatus document={item} />
|
|
175
|
+
<EditButton
|
|
176
|
+
id={item._id}
|
|
177
|
+
type={item._type}
|
|
178
|
+
disabled={!userRoleCanDrop}
|
|
82
179
|
/>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
180
|
+
{isLastState && states.length <= 3 ? (
|
|
181
|
+
<CompleteButton
|
|
182
|
+
documentId={documentId}
|
|
183
|
+
disabled={!userRoleCanDrop}
|
|
184
|
+
/>
|
|
185
|
+
) : null}
|
|
186
|
+
</Flex>
|
|
187
|
+
{isLastState && states.length > 3 ? (
|
|
188
|
+
<Stack paddingTop={2}>
|
|
189
|
+
<CompleteButton
|
|
190
|
+
documentId={documentId}
|
|
191
|
+
disabled={!userRoleCanDrop}
|
|
192
|
+
/>
|
|
193
|
+
</Stack>
|
|
194
|
+
) : null}
|
|
195
|
+
</Card>
|
|
196
|
+
</Stack>
|
|
197
|
+
</Card>
|
|
198
|
+
</Box>
|
|
199
|
+
</>
|
|
91
200
|
)
|
|
92
201
|
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {Draggable, DraggableStyle} from '@hello-pangea/dnd'
|
|
2
|
+
import {useVirtualizer, VirtualItem} from '@tanstack/react-virtual'
|
|
3
|
+
import {CSSProperties, useMemo, useRef} from 'react'
|
|
4
|
+
import {CurrentUser} from 'sanity'
|
|
5
|
+
import {UserExtended} from 'sanity-plugin-utils'
|
|
6
|
+
|
|
7
|
+
import {filterItemsAndSort} from '../helpers/filterItemsAndSort'
|
|
8
|
+
import {SanityDocumentWithMetadata, State} from '../types'
|
|
9
|
+
import {DocumentCard} from './DocumentCard'
|
|
10
|
+
|
|
11
|
+
type DocumentListProps = {
|
|
12
|
+
data: SanityDocumentWithMetadata[]
|
|
13
|
+
invalidDocumentIds: string[]
|
|
14
|
+
patchingIds: string[]
|
|
15
|
+
selectedSchemaTypes: string[]
|
|
16
|
+
selectedUserIds: string[]
|
|
17
|
+
state: State
|
|
18
|
+
states: State[]
|
|
19
|
+
toggleInvalidDocumentId: (
|
|
20
|
+
documentId: string,
|
|
21
|
+
action: 'ADD' | 'REMOVE'
|
|
22
|
+
) => void
|
|
23
|
+
user: CurrentUser | null
|
|
24
|
+
userList: UserExtended[]
|
|
25
|
+
userRoleCanDrop: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getStyle(
|
|
29
|
+
draggableStyle: DraggableStyle | undefined,
|
|
30
|
+
virtualItem: VirtualItem
|
|
31
|
+
): CSSProperties {
|
|
32
|
+
// Default transform required by tanstack virtual for positioning
|
|
33
|
+
let transform = `translateY(${virtualItem.start}px)`
|
|
34
|
+
|
|
35
|
+
// If a card is being dragged over, this card needs to move up or down
|
|
36
|
+
if (draggableStyle && draggableStyle.transform) {
|
|
37
|
+
// So get the transform value from beautiful-dnd
|
|
38
|
+
const draggableTransformY = parseInt(
|
|
39
|
+
draggableStyle.transform.split(',')[1].split('px')[0],
|
|
40
|
+
10
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// And apply it to the card
|
|
44
|
+
transform = `translateY(${virtualItem.start + draggableTransformY}px)`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
position: 'absolute',
|
|
49
|
+
top: 0,
|
|
50
|
+
left: 0,
|
|
51
|
+
width: '100%',
|
|
52
|
+
height: `${virtualItem.size}px`,
|
|
53
|
+
transform,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default function DocumentList(props: DocumentListProps) {
|
|
58
|
+
const {
|
|
59
|
+
data = [],
|
|
60
|
+
invalidDocumentIds,
|
|
61
|
+
patchingIds,
|
|
62
|
+
selectedSchemaTypes,
|
|
63
|
+
selectedUserIds,
|
|
64
|
+
state,
|
|
65
|
+
states,
|
|
66
|
+
toggleInvalidDocumentId,
|
|
67
|
+
user,
|
|
68
|
+
userList,
|
|
69
|
+
userRoleCanDrop,
|
|
70
|
+
} = props
|
|
71
|
+
|
|
72
|
+
const dataFiltered = useMemo(() => {
|
|
73
|
+
return data.length
|
|
74
|
+
? filterItemsAndSort(data, state.id, selectedUserIds, selectedSchemaTypes)
|
|
75
|
+
: []
|
|
76
|
+
}, [data, selectedSchemaTypes, selectedUserIds, state.id])
|
|
77
|
+
|
|
78
|
+
const parentRef = useRef(null)
|
|
79
|
+
|
|
80
|
+
const virtualizer = useVirtualizer({
|
|
81
|
+
count: dataFiltered.length,
|
|
82
|
+
getScrollElement: () => parentRef.current,
|
|
83
|
+
getItemKey: (index) => dataFiltered[index]?._metadata?.documentId ?? index,
|
|
84
|
+
estimateSize: () => 115,
|
|
85
|
+
overscan: 7,
|
|
86
|
+
measureElement: (element) => {
|
|
87
|
+
return element.getBoundingClientRect().height || 115
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
if (!data.length || !dataFiltered.length) {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
ref={parentRef}
|
|
98
|
+
style={{
|
|
99
|
+
height: `100%`,
|
|
100
|
+
overflow: 'auto',
|
|
101
|
+
// Smooths scrollbar behaviour
|
|
102
|
+
overflowAnchor: 'none',
|
|
103
|
+
scrollBehavior: 'auto',
|
|
104
|
+
paddingTop: 1,
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
<div
|
|
108
|
+
style={{
|
|
109
|
+
height: `${virtualizer.getTotalSize()}px`,
|
|
110
|
+
width: '100%',
|
|
111
|
+
position: 'relative',
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
{virtualizer.getVirtualItems().map((virtualItem) => {
|
|
115
|
+
const item = dataFiltered[virtualItem.index]
|
|
116
|
+
|
|
117
|
+
const {documentId, assignees} = item?._metadata ?? {}
|
|
118
|
+
|
|
119
|
+
const isInvalid = invalidDocumentIds.includes(documentId)
|
|
120
|
+
const meInAssignees = user?.id ? assignees?.includes(user.id) : false
|
|
121
|
+
const isDragDisabled =
|
|
122
|
+
patchingIds.includes(documentId) ||
|
|
123
|
+
!userRoleCanDrop ||
|
|
124
|
+
isInvalid ||
|
|
125
|
+
!(state.requireAssignment
|
|
126
|
+
? state.requireAssignment && meInAssignees
|
|
127
|
+
: true)
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<Draggable
|
|
131
|
+
key={virtualItem.key}
|
|
132
|
+
draggableId={documentId}
|
|
133
|
+
index={virtualItem.index}
|
|
134
|
+
isDragDisabled={isDragDisabled}
|
|
135
|
+
>
|
|
136
|
+
{(draggableProvided, draggableSnapshot) => (
|
|
137
|
+
<div
|
|
138
|
+
ref={draggableProvided.innerRef}
|
|
139
|
+
{...draggableProvided.draggableProps}
|
|
140
|
+
{...draggableProvided.dragHandleProps}
|
|
141
|
+
style={getStyle(
|
|
142
|
+
draggableProvided.draggableProps.style,
|
|
143
|
+
virtualItem
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
<div
|
|
147
|
+
ref={virtualizer.measureElement}
|
|
148
|
+
data-index={virtualItem.index}
|
|
149
|
+
>
|
|
150
|
+
<DocumentCard
|
|
151
|
+
userRoleCanDrop={userRoleCanDrop}
|
|
152
|
+
isDragDisabled={isDragDisabled}
|
|
153
|
+
isPatching={patchingIds.includes(documentId)}
|
|
154
|
+
isDragging={draggableSnapshot.isDragging}
|
|
155
|
+
item={item}
|
|
156
|
+
toggleInvalidDocumentId={toggleInvalidDocumentId}
|
|
157
|
+
userList={userList}
|
|
158
|
+
states={states}
|
|
159
|
+
/>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</Draggable>
|
|
164
|
+
)
|
|
165
|
+
})}
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {ResetIcon, UserIcon} from '@sanity/icons'
|
|
2
|
+
import {Button, Card, Flex, Menu, MenuButton} from '@sanity/ui'
|
|
3
|
+
import {useCallback} from 'react'
|
|
4
|
+
import {useCurrentUser, UserAvatar, useSchema} from 'sanity'
|
|
5
|
+
import {UserExtended, UserSelectMenu} from 'sanity-plugin-utils'
|
|
6
|
+
|
|
7
|
+
type FiltersProps = {
|
|
8
|
+
uniqueAssignedUsers: UserExtended[]
|
|
9
|
+
selectedUserIds: string[]
|
|
10
|
+
schemaTypes: string[]
|
|
11
|
+
selectedSchemaTypes: string[]
|
|
12
|
+
toggleSelectedUser: (userId: string) => void
|
|
13
|
+
resetSelectedUsers: () => void
|
|
14
|
+
toggleSelectedSchemaType: (schemaType: string) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function Filters(props: FiltersProps) {
|
|
18
|
+
const {
|
|
19
|
+
uniqueAssignedUsers = [],
|
|
20
|
+
selectedUserIds,
|
|
21
|
+
schemaTypes,
|
|
22
|
+
selectedSchemaTypes,
|
|
23
|
+
toggleSelectedUser,
|
|
24
|
+
resetSelectedUsers,
|
|
25
|
+
toggleSelectedSchemaType,
|
|
26
|
+
} = props
|
|
27
|
+
|
|
28
|
+
const currentUser = useCurrentUser()
|
|
29
|
+
const schema = useSchema()
|
|
30
|
+
|
|
31
|
+
const onAdd = useCallback(
|
|
32
|
+
(id: string) => {
|
|
33
|
+
if (!selectedUserIds.includes(id)) {
|
|
34
|
+
toggleSelectedUser(id)
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
[selectedUserIds, toggleSelectedUser]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const onRemove = useCallback(
|
|
41
|
+
(id: string) => {
|
|
42
|
+
if (selectedUserIds.includes(id)) {
|
|
43
|
+
toggleSelectedUser(id)
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
[selectedUserIds, toggleSelectedUser]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const onClear = useCallback(() => {
|
|
50
|
+
resetSelectedUsers()
|
|
51
|
+
}, [resetSelectedUsers])
|
|
52
|
+
|
|
53
|
+
if (uniqueAssignedUsers.length === 0 && schemaTypes.length < 2) {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const meInUniqueAssignees =
|
|
58
|
+
currentUser?.id && uniqueAssignedUsers.find((u) => u.id === currentUser.id)
|
|
59
|
+
const uniqueAssigneesNotMe = uniqueAssignedUsers.filter(
|
|
60
|
+
(u) => u.id !== currentUser?.id
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Card tone="primary" padding={2} borderBottom style={{overflowX: 'hidden'}}>
|
|
65
|
+
<Flex align="center">
|
|
66
|
+
<Flex align="center" gap={1} flex={1}>
|
|
67
|
+
{uniqueAssignedUsers.length > 5 ? (
|
|
68
|
+
<Card tone="default">
|
|
69
|
+
<MenuButton
|
|
70
|
+
button={
|
|
71
|
+
<Button
|
|
72
|
+
text="Filter Assignees"
|
|
73
|
+
tone="primary"
|
|
74
|
+
icon={UserIcon}
|
|
75
|
+
/>
|
|
76
|
+
}
|
|
77
|
+
id="user-filters"
|
|
78
|
+
menu={
|
|
79
|
+
<Menu>
|
|
80
|
+
<UserSelectMenu
|
|
81
|
+
value={selectedUserIds}
|
|
82
|
+
userList={uniqueAssignedUsers}
|
|
83
|
+
onAdd={onAdd}
|
|
84
|
+
onRemove={onRemove}
|
|
85
|
+
onClear={onClear}
|
|
86
|
+
labels={{
|
|
87
|
+
addMe: 'Filter mine',
|
|
88
|
+
removeMe: 'Clear mine',
|
|
89
|
+
clear: 'Clear filters',
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
</Menu>
|
|
93
|
+
}
|
|
94
|
+
popover={{portal: true}}
|
|
95
|
+
/>
|
|
96
|
+
</Card>
|
|
97
|
+
) : (
|
|
98
|
+
<>
|
|
99
|
+
{meInUniqueAssignees ? (
|
|
100
|
+
<>
|
|
101
|
+
<Button
|
|
102
|
+
padding={0}
|
|
103
|
+
mode={
|
|
104
|
+
selectedUserIds.includes(currentUser.id)
|
|
105
|
+
? `default`
|
|
106
|
+
: `bleed`
|
|
107
|
+
}
|
|
108
|
+
onClick={() => toggleSelectedUser(currentUser.id)}
|
|
109
|
+
>
|
|
110
|
+
<Flex padding={1} align="center" justify="center">
|
|
111
|
+
<UserAvatar user={currentUser.id} size={1} withTooltip />
|
|
112
|
+
</Flex>
|
|
113
|
+
</Button>
|
|
114
|
+
<Card borderRight style={{height: 30}} tone="inherit" />
|
|
115
|
+
</>
|
|
116
|
+
) : null}
|
|
117
|
+
{uniqueAssigneesNotMe.map((user) => (
|
|
118
|
+
<Button
|
|
119
|
+
key={user.id}
|
|
120
|
+
padding={0}
|
|
121
|
+
mode={selectedUserIds.includes(user.id) ? `default` : `bleed`}
|
|
122
|
+
onClick={() => toggleSelectedUser(user.id)}
|
|
123
|
+
>
|
|
124
|
+
<Flex padding={1} align="center" justify="center">
|
|
125
|
+
<UserAvatar user={user} size={1} withTooltip />
|
|
126
|
+
</Flex>
|
|
127
|
+
</Button>
|
|
128
|
+
))}
|
|
129
|
+
|
|
130
|
+
{selectedUserIds.length > 0 ? (
|
|
131
|
+
<Button
|
|
132
|
+
text="Clear"
|
|
133
|
+
onClick={resetSelectedUsers}
|
|
134
|
+
mode="ghost"
|
|
135
|
+
icon={ResetIcon}
|
|
136
|
+
/>
|
|
137
|
+
) : null}
|
|
138
|
+
</>
|
|
139
|
+
)}
|
|
140
|
+
</Flex>
|
|
141
|
+
|
|
142
|
+
{schemaTypes.length > 1 ? (
|
|
143
|
+
<Flex align="center" gap={1}>
|
|
144
|
+
{schemaTypes.map((typeName) => {
|
|
145
|
+
const schemaType = schema.get(typeName)
|
|
146
|
+
|
|
147
|
+
if (!schemaType) {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Button
|
|
153
|
+
key={typeName}
|
|
154
|
+
text={schemaType?.title ?? typeName}
|
|
155
|
+
icon={schemaType?.icon ?? undefined}
|
|
156
|
+
mode={
|
|
157
|
+
selectedSchemaTypes.includes(typeName) ? `default` : `ghost`
|
|
158
|
+
}
|
|
159
|
+
onClick={() => toggleSelectedSchemaType(typeName)}
|
|
160
|
+
/>
|
|
161
|
+
)
|
|
162
|
+
})}
|
|
163
|
+
</Flex>
|
|
164
|
+
) : null}
|
|
165
|
+
</Flex>
|
|
166
|
+
</Card>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {PropsWithChildren} from 'react'
|
|
2
|
+
import styled, {css} from 'styled-components'
|
|
3
|
+
import {Card, Grid} from '@sanity/ui'
|
|
4
|
+
import {motion, AnimatePresence} from 'framer-motion'
|
|
5
|
+
|
|
6
|
+
const StyledFloatingCard = styled(Card)(
|
|
7
|
+
() => css`
|
|
8
|
+
position: fixed;
|
|
9
|
+
bottom: 0;
|
|
10
|
+
left: 0;
|
|
11
|
+
z-index: 1000;
|
|
12
|
+
`
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
export default function FloatingCard({children}: PropsWithChildren) {
|
|
16
|
+
const childrenHaveValues = Array.isArray(children) ? children.some(Boolean) : Boolean(children)
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<AnimatePresence>
|
|
20
|
+
{childrenHaveValues ? (
|
|
21
|
+
<motion.div key="floater" initial={{opacity: 0}} animate={{opacity: 1}} exit={{opacity: 0}}>
|
|
22
|
+
<StyledFloatingCard shadow={3} padding={3} margin={3} radius={3}>
|
|
23
|
+
<Grid gap={2}>{children}</Grid>
|
|
24
|
+
</StyledFloatingCard>
|
|
25
|
+
</motion.div>
|
|
26
|
+
) : null}
|
|
27
|
+
</AnimatePresence>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import {Box, Text, Tooltip} from '@sanity/ui'
|
|
3
|
+
|
|
4
|
+
type StatusProps = {
|
|
5
|
+
text: string
|
|
6
|
+
icon: React.ComponentType
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Status(props: StatusProps) {
|
|
10
|
+
const {text, icon} = props
|
|
11
|
+
const Icon = icon
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Tooltip
|
|
15
|
+
portal
|
|
16
|
+
content={
|
|
17
|
+
<Box padding={2}>
|
|
18
|
+
<Text size={1}>{text}</Text>
|
|
19
|
+
</Box>
|
|
20
|
+
}
|
|
21
|
+
>
|
|
22
|
+
<Text size={1}>
|
|
23
|
+
<Icon />
|
|
24
|
+
</Text>
|
|
25
|
+
</Tooltip>
|
|
26
|
+
)
|
|
27
|
+
}
|