Skip to content

Commit d1ffc55

Browse files
committed
feat(tasks): create searchUsersMenu shared component
1 parent 73a7316 commit d1ffc55

File tree

5 files changed

+223
-338
lines changed

5 files changed

+223
-338
lines changed
Lines changed: 6 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
1-
import {
2-
AvatarStack,
3-
// eslint-disable-next-line no-restricted-imports
4-
Button as UIButton,
5-
Flex,
6-
} from '@sanity/ui'
7-
import {AnimatePresence, motion} from 'framer-motion'
8-
import {useCallback, useMemo} from 'react'
1+
import {Flex} from '@sanity/ui'
2+
import {useCallback} from 'react'
93
import {type FormPatch, type PatchEvent, type Path, set} from 'sanity'
104

115
import {Button} from '../../../../../ui-components'
126
import {type TaskDocument} from '../../types'
13-
import {TasksUserAvatar} from '../TasksUserAvatar'
147
import {TasksSubscribersMenu} from './TasksSubscribersMenu'
158

169
interface TasksSubscriberProps {
@@ -20,6 +13,8 @@ interface TasksSubscriberProps {
2013
currentUserId: string
2114
}
2215

16+
const EMPTY_ARRAY: [] = []
17+
2318
export function TasksSubscribers(props: TasksSubscriberProps) {
2419
const {value, onChange, path, currentUserId} = props
2520

@@ -53,66 +48,10 @@ export function TasksSubscribers(props: TasksSubscriberProps) {
5348
return (
5449
<Flex gap={1} align="center">
5550
<Button mode="bleed" text={buttonText} onClick={handleToggleSubscribe} />
56-
57-
<TasksSubscriberAvatars
58-
subscriberIds={value.subscribers}
51+
<TasksSubscribersMenu
52+
value={value.subscribers?.filter(Boolean) || EMPTY_ARRAY}
5953
handleUserSubscriptionChange={handleUserSubscriptionChange}
6054
/>
6155
</Flex>
6256
)
6357
}
64-
65-
const EMPTY_ARRAY: [] = []
66-
67-
interface TasksSubscriberAvatarsProps {
68-
subscriberIds?: string[]
69-
handleUserSubscriptionChange: (userId: string) => void
70-
}
71-
72-
export function TasksSubscriberAvatars(props: TasksSubscriberAvatarsProps) {
73-
const {subscriberIds: subscriberIdsProp, handleUserSubscriptionChange} = props
74-
75-
const subscriberIds = useMemo(() => {
76-
// Make sure we have valid subscriber IDs
77-
return subscriberIdsProp?.filter(Boolean) || EMPTY_ARRAY
78-
}, [subscriberIdsProp])
79-
80-
const onSelect = useCallback(
81-
(userId: string) => handleUserSubscriptionChange(userId),
82-
[handleUserSubscriptionChange],
83-
)
84-
85-
return (
86-
<TasksSubscribersMenu
87-
menuButton={
88-
<UIButton type="button" mode="bleed" padding={1}>
89-
{subscriberIds.length > 0 ? (
90-
<AnimatePresence initial={false}>
91-
<AvatarStack maxLength={3} size={0}>
92-
{subscriberIds.map((subscriberId) => (
93-
<motion.div
94-
key={subscriberId}
95-
exit={{opacity: 0, translateX: '2px', scale: 0.9}}
96-
animate={{
97-
opacity: 1,
98-
translateX: 0,
99-
scale: 1,
100-
transition: {type: 'just', duration: 0.2},
101-
}}
102-
initial={{opacity: 0, translateX: '2px', scale: 0.9}}
103-
>
104-
<TasksUserAvatar user={{id: subscriberId}} size={0} />
105-
</motion.div>
106-
))}
107-
</AvatarStack>
108-
</AnimatePresence>
109-
) : (
110-
<TasksUserAvatar size={0} />
111-
)}
112-
</UIButton>
113-
}
114-
value={subscriberIds}
115-
onSelect={onSelect}
116-
/>
117-
)
118-
}

packages/sanity/src/tasks/src/tasks/components/activity/TasksSubscribersMenu.tsx

Lines changed: 63 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,29 @@
1-
import {UserIcon} from '@sanity/icons'
21
import {
2+
AvatarStack,
33
Box,
4+
// eslint-disable-next-line no-restricted-imports
5+
Button as UIButton,
46
Checkbox,
5-
Container,
67
Flex,
78
Menu,
8-
MenuDivider,
99
// eslint-disable-next-line no-restricted-imports
1010
MenuItem,
1111
Text,
12-
TextInput,
1312
} from '@sanity/ui'
14-
import {
15-
type ChangeEvent,
16-
type KeyboardEvent,
17-
type MouseEvent,
18-
useCallback,
19-
useMemo,
20-
useRef,
21-
useState,
22-
} from 'react'
23-
import {LoadingBlock, type UserWithPermission, useTranslation} from 'sanity'
13+
import {AnimatePresence, motion} from 'framer-motion'
14+
import {type MouseEvent, useCallback, useMemo, useState} from 'react'
15+
import {type UserWithPermission, useTranslation} from 'sanity'
2416
import {styled} from 'styled-components'
2517

2618
import {MenuButton} from '../../../../../ui-components'
2719
import {tasksLocaleNamespace} from '../../../../i18n'
2820
import {useMentionUser} from '../../context'
29-
import {useFilteredOptions} from '../form/fields/assignee/useFilteredOptions'
21+
import {SearchUsersMenu} from '../searchUsersMenu/SearchUsersMenu'
3022
import {TasksUserAvatar} from '../TasksUserAvatar'
3123

3224
type SelectItemHandler = (id: string) => void
3325

34-
function MentionUserMenuItem(props: {
26+
function SubscriberUserMenuItem(props: {
3527
user: UserWithPermission
3628
onSelect: SelectItemHandler
3729
selected: boolean
@@ -41,6 +33,7 @@ function MentionUserMenuItem(props: {
4133

4234
const handleCheckboxClick = useCallback(
4335
(e: MouseEvent<HTMLDivElement>) => {
36+
// Stops propagation to avoid closing the menu. When clicking the checkbox we want to keep the menu open.
4437
e.stopPropagation()
4538
handleSelect()
4639
},
@@ -68,53 +61,40 @@ const StyledMenu = styled(Menu)`
6861
width: 308px;
6962
border-radius: 3px;
7063
`
64+
interface TasksSubscriberMenuProps {
65+
value?: string[]
66+
handleUserSubscriptionChange: (userId: string) => void
67+
}
7168

72-
const IGNORED_KEYS = [
73-
'Control',
74-
'Shift',
75-
'Alt',
76-
'Enter',
77-
'Home',
78-
'End',
79-
'PageUp',
80-
'PageDown',
81-
'Meta',
82-
'Tab',
83-
'CapsLock',
84-
]
69+
export function TasksSubscribersMenu(props: TasksSubscriberMenuProps) {
70+
const {value = [], handleUserSubscriptionChange} = props
8571

86-
function TasksSubscribers({onSelect, value = []}: {onSelect: SelectItemHandler; value?: string[]}) {
87-
const [searchTerm, setSearchTerm] = useState<string>('')
72+
const onSelect = useCallback(
73+
(userId: string) => handleUserSubscriptionChange(userId),
74+
[handleUserSubscriptionChange],
75+
)
76+
77+
const {t} = useTranslation(tasksLocaleNamespace)
8878
const {mentionOptions} = useMentionUser()
89-
const inputRef = useRef<HTMLInputElement | null>(null)
9079
// This list will keep a local state of users who are initially subscribed and later added or removed.
91-
// To always render them at the top
80+
// rendering them always at the top.
9281
const [subscribersList, setSubscribersList] = useState(value)
9382

94-
const handleSearchChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
95-
setSearchTerm(event.currentTarget.value)
96-
}, [])
97-
98-
const filteredOptions = useFilteredOptions({options: mentionOptions.data || [], searchTerm})
99-
100-
const selectedUsers = useMemo(
101-
() => filteredOptions.filter((user) => subscribersList.includes(user.id)),
102-
[filteredOptions, subscribersList],
103-
)
104-
10583
const handleSelect = useCallback(
10684
(id: string) => {
10785
if (!subscribersList.includes(id)) {
86+
// Persist user id in local subscribers list state.
10887
setSubscribersList([...subscribersList, id])
10988
}
11089
onSelect(id)
11190
},
11291
[subscribersList, onSelect],
11392
)
93+
11494
const renderItem = useCallback(
11595
(user: UserWithPermission) => {
11696
return (
117-
<MentionUserMenuItem
97+
<SubscriberUserMenuItem
11898
user={user}
11999
onSelect={handleSelect}
120100
key={user.id}
@@ -124,79 +104,52 @@ function TasksSubscribers({onSelect, value = []}: {onSelect: SelectItemHandler;
124104
},
125105
[handleSelect, value],
126106
)
127-
const handleKeyDown = useCallback((event: KeyboardEvent<HTMLElement>) => {
128-
// If target is input don't do anything
129-
if (event.target === inputRef.current) {
130-
return
131-
}
132-
133-
if (!IGNORED_KEYS.includes(event.key)) {
134-
inputRef.current?.focus()
135-
}
136-
}, [])
137-
138-
const {t} = useTranslation(tasksLocaleNamespace)
139-
140-
if (mentionOptions.loading) {
141-
return (
142-
<Container width={0}>
143-
<LoadingBlock showText />
144-
</Container>
145-
)
146-
}
147-
148-
return (
149-
<div onKeyDown={handleKeyDown} style={{maxHeight: '360px', width: '100%'}}>
150-
<Box paddingBottom={2}>
151-
<TextInput
152-
placeholder={t('form.subscribers.menu.input.placeholder')}
153-
autoFocus
154-
border={false}
155-
onChange={handleSearchChange}
156-
value={searchTerm}
157-
fontSize={1}
158-
icon={UserIcon}
159-
ref={inputRef}
160-
/>
161-
</Box>
162107

163-
{filteredOptions.length === 0 ? (
164-
<Box padding={3}>
165-
<Text align="center" size={1} muted>
166-
{t('form.input.assignee.search.no-users.text')}
167-
</Text>
168-
</Box>
169-
) : (
170-
<>
171-
{!searchTerm && selectedUsers.length > 0 && (
172-
<>
173-
{selectedUsers.map(renderItem)}
174-
<Box paddingY={2}>
175-
<MenuDivider />
176-
</Box>
177-
</>
178-
)}
179-
{filteredOptions.map(renderItem)}
180-
</>
181-
)}
182-
</div>
108+
const selectedUsers = useMemo(
109+
() => mentionOptions.data?.filter((user) => subscribersList.includes(user.id)),
110+
[mentionOptions, subscribersList],
183111
)
184-
}
185-
186-
export function TasksSubscribersMenu(props: {
187-
onSelect: (userId: string) => void
188-
menuButton: React.ReactElement
189-
value?: string[]
190-
}) {
191-
const {onSelect, menuButton, value} = props
192112

193113
return (
194114
<MenuButton
195-
button={menuButton}
115+
button={
116+
<UIButton type="button" mode="bleed" padding={1}>
117+
{value.length > 0 ? (
118+
<AnimatePresence initial={false}>
119+
<AvatarStack maxLength={3} size={0}>
120+
{value.map((subscriberId) => (
121+
<motion.div
122+
key={subscriberId}
123+
exit={{opacity: 0, translateX: '2px', scale: 0.9}}
124+
animate={{
125+
opacity: 1,
126+
translateX: 0,
127+
scale: 1,
128+
transition: {type: 'just', duration: 0.2},
129+
}}
130+
initial={{opacity: 0, translateX: '2px', scale: 0.9}}
131+
>
132+
<TasksUserAvatar user={{id: subscriberId}} size={0} />
133+
</motion.div>
134+
))}
135+
</AvatarStack>
136+
</AnimatePresence>
137+
) : (
138+
<TasksUserAvatar size={0} />
139+
)}
140+
</UIButton>
141+
}
196142
id="assign-user-menu"
197143
menu={
198144
<StyledMenu>
199-
<TasksSubscribers onSelect={onSelect} value={value} />
145+
<SearchUsersMenu
146+
renderItem={renderItem}
147+
selectedUsers={selectedUsers}
148+
loading={mentionOptions.loading}
149+
options={mentionOptions.data || []}
150+
name="subscribersSearch"
151+
placeholder={t('form.subscribers.menu.input.placeholder')}
152+
/>
200153
</StyledMenu>
201154
}
202155
popover={{

0 commit comments

Comments
 (0)