1- import  { UserIcon }  from  '@sanity/icons' 
21import  { 
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' 
2416import  { styled }  from  'styled-components' 
2517
2618import  { MenuButton }  from  '../../../../../ui-components' 
2719import  { tasksLocaleNamespace }  from  '../../../../i18n' 
2820import  { useMentionUser }  from  '../../context' 
29- import  { useFilteredOptions }  from  '../form/fields/assignee/useFilteredOptions ' 
21+ import  { SearchUsersMenu }  from  '../searchUsersMenu/SearchUsersMenu ' 
3022import  { TasksUserAvatar }  from  '../TasksUserAvatar' 
3123
3224type  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