@@ -16,8 +16,6 @@ import { Input } from '@/components/ui/input';
1616import { Label } from '@/components/ui/label' ;
1717import { Textarea } from '@/components/ui/textarea' ;
1818import { Badge } from '@/components/ui/badge' ;
19- import { Command , CommandEmpty , CommandGroup , CommandItem , CommandList } from '@/components/ui/command' ;
20- import { Popover , PopoverContent , PopoverTrigger } from '@/components/ui/popover' ;
2119import { KbdHint } from '@/components/ui/KbdHint' ;
2220import {
2321 Select ,
@@ -107,6 +105,7 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
107105 const [ suggestionsOpen , setSuggestionsOpen ] = useState ( false ) ;
108106 const [ loadingSuggestions , setLoadingSuggestions ] = useState ( false ) ;
109107 const [ selectedProfile , setSelectedProfile ] = useState < UserSuggestion | null > ( null ) ;
108+ const [ shareUserError , setShareUserError ] = useState ( '' ) ;
110109 const [ sharePermission , setSharePermission ] = useState < 'viewer' | 'editor' > ( 'viewer' ) ;
111110 const [ publicSlug , setPublicSlug ] = useState ( '' ) ;
112111 const [ slugAvailable , setSlugAvailable ] = useState < boolean | null > ( null ) ;
@@ -334,12 +333,14 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
334333 const handleSelectUserSuggestion = useCallback ( ( profile : UserSuggestion ) => {
335334 setSelectedProfile ( profile ) ;
336335 setEmail ( profile . email || '' ) ;
336+ setShareUserError ( '' ) ;
337337 setSuggestionsOpen ( false ) ;
338338 } , [ ] ) ;
339339
340340 const handleShareEmailChange = useCallback ( ( value : string ) => {
341341 setEmail ( value ) ;
342342 setSelectedProfile ( null ) ;
343+ setShareUserError ( '' ) ;
343344 } , [ ] ) ;
344345
345346 useEffect ( ( ) => {
@@ -672,44 +673,62 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
672673
673674 setSaving ( true ) ;
674675 try {
675- // Use the selected autocomplete profile when available; otherwise fall back to email lookup.
676+ // Use the selected autocomplete profile when available; otherwise require an exact
677+ // platform-user match by email or username before inserting a share.
676678 let profile = selectedProfile ;
679+ const query = email . trim ( ) . toLowerCase ( ) ;
677680 if ( ! profile ) {
678- const { data : profileData } = await supabase
681+ const { data : emailMatch , error : emailLookupError } = await supabase
679682 . from ( 'profiles' )
680683 . select ( 'user_id, display_name, username, email' )
681- . eq ( 'email' , email . trim ( ) . toLowerCase ( ) )
684+ . eq ( 'email' , query )
682685 . maybeSingle ( ) ;
683- profile = profileData as UserSuggestion | null ;
686+
687+ if ( emailLookupError ) throw emailLookupError ;
688+
689+ if ( emailMatch ) {
690+ profile = emailMatch as UserSuggestion ;
691+ } else {
692+ const { data : usernameMatch , error : usernameLookupError } = await supabase
693+ . from ( 'profiles' )
694+ . select ( 'user_id, display_name, username, email' )
695+ . eq ( 'username' , query )
696+ . maybeSingle ( ) ;
697+
698+ if ( usernameLookupError ) throw usernameLookupError ;
699+ profile = usernameMatch as UserSuggestion | null ;
700+ }
701+ }
702+
703+ if ( ! profile ?. user_id ) {
704+ setShareUserError ( '// error no user found' ) ;
705+ return ;
684706 }
685707
686708 const shareData : {
687709 vault_id : string ;
688710 shared_with_email : string ;
689711 shared_by : string ;
690712 role : 'viewer' | 'editor' ;
691- shared_with_user_id ? : string ;
713+ shared_with_user_id : string ;
692714 shared_with_name ?: string | null ;
693715 } = {
694716 vault_id : vault . id ,
695- shared_with_email : email . trim ( ) . toLowerCase ( ) ,
717+ shared_with_email : ( profile . email || query ) . toLowerCase ( ) ,
696718 shared_by : user . id ,
697719 role : sharePermission ,
720+ shared_with_user_id : profile . user_id ,
721+ shared_with_name : profile . display_name || profile . username || profile . email ,
698722 } ;
699723
700- // If we found a profile, add the user_id and display name
701- if ( profile ) {
702- shareData . shared_with_user_id = profile . user_id ;
703- shareData . shared_with_name = profile . display_name || profile . username || profile . email ;
704- }
705-
706724 const { error } = await supabase . from ( 'vault_shares' ) . insert ( shareData ) ;
707725
708726 if ( error ) throw error ;
709727
710728 toast ( { title : 'user_added ✨' } ) ;
711729 setEmail ( '' ) ;
712730 setSelectedProfile ( null ) ;
731+ setShareUserError ( '' ) ;
713732 setUserSuggestions ( [ ] ) ;
714733 setSuggestionsOpen ( false ) ;
715734 setSharePermission ( 'viewer' ) ;
@@ -1054,59 +1073,52 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
10541073
10551074 < div className = "space-y-3" >
10561075 < div className = "flex gap-2" >
1057- < Popover open = { suggestionsOpen } onOpenChange = { setSuggestionsOpen } >
1058- < PopoverTrigger asChild >
1059- < div className = "relative flex-1" >
1060- < Mail className = "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
1061- < Input
1062- type = "text"
1063- value = { email }
1064- onChange = { ( e ) => handleShareEmailChange ( e . target . value ) }
1065- onFocus = { ( ) => {
1066- if ( email . trim ( ) . length >= 2 ) setSuggestionsOpen ( true ) ;
1067- } }
1068- placeholder = "name, username, or email"
1069- className = "pl-10 font-mono text-sm"
1070- autoComplete = "off"
1071- autoCapitalize = "none"
1072- spellCheck = { false }
1073- />
1076+ < div className = "relative flex-1" >
1077+ < Mail className = "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
1078+ < Input
1079+ type = "text"
1080+ value = { email }
1081+ onChange = { ( e ) => handleShareEmailChange ( e . target . value ) }
1082+ onFocus = { ( ) => {
1083+ if ( email . trim ( ) . length >= 2 ) setSuggestionsOpen ( true ) ;
1084+ } }
1085+ onBlur = { ( ) => window . setTimeout ( ( ) => setSuggestionsOpen ( false ) , 120 ) }
1086+ placeholder = "name, username, or email"
1087+ className = "pl-10 font-mono text-sm"
1088+ autoComplete = "off"
1089+ autoCapitalize = "none"
1090+ spellCheck = { false }
1091+ />
1092+ { suggestionsOpen && ( loadingSuggestions || userSuggestions . length > 0 ) && (
1093+ < div className = "absolute left-0 right-0 top-[calc(100%+0.25rem)] z-50 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md" >
1094+ { loadingSuggestions ? (
1095+ < div className = "px-3 py-2 text-sm text-muted-foreground font-mono" > loading_users…</ div >
1096+ ) : (
1097+ < div className = "space-y-1" >
1098+ < div className = "px-2 py-1 text-xs uppercase tracking-wide text-muted-foreground font-mono" > matching_users</ div >
1099+ { userSuggestions . map ( ( profile ) => (
1100+ < button
1101+ key = { profile . user_id }
1102+ type = "button"
1103+ onMouseDown = { ( event ) => event . preventDefault ( ) }
1104+ onClick = { ( ) => handleSelectUserSuggestion ( profile ) }
1105+ className = "flex w-full flex-col rounded-sm px-2 py-2 text-left font-mono text-sm hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none"
1106+ >
1107+ < span className = "truncate" >
1108+ { profile . display_name || profile . username || profile . email }
1109+ </ span >
1110+ { profile . email && (
1111+ < span className = "text-xs text-muted-foreground truncate" >
1112+ { profile . email }
1113+ </ span >
1114+ ) }
1115+ </ button >
1116+ ) ) }
1117+ </ div >
1118+ ) }
10741119 </ div >
1075- </ PopoverTrigger >
1076- < PopoverContent className = "w-[var(--radix-popover-trigger-width)] p-0" align = "start" >
1077- < Command shouldFilter = { false } >
1078- < CommandList >
1079- { loadingSuggestions ? (
1080- < CommandEmpty > loading_users…</ CommandEmpty >
1081- ) : userSuggestions . length === 0 ? (
1082- < CommandEmpty > no_matching_users</ CommandEmpty >
1083- ) : (
1084- < CommandGroup heading = "matching_users" >
1085- { userSuggestions . map ( ( profile ) => (
1086- < CommandItem
1087- key = { profile . user_id }
1088- value = { `${ profile . email || '' } ${ profile . display_name || '' } ${ profile . username || '' } ` }
1089- onSelect = { ( ) => handleSelectUserSuggestion ( profile ) }
1090- className = "font-mono text-sm"
1091- >
1092- < div className = "flex flex-col min-w-0" >
1093- < span className = "truncate" >
1094- { profile . display_name || profile . username || profile . email }
1095- </ span >
1096- { profile . email && (
1097- < span className = "text-xs text-muted-foreground truncate" >
1098- { profile . email }
1099- </ span >
1100- ) }
1101- </ div >
1102- </ CommandItem >
1103- ) ) }
1104- </ CommandGroup >
1105- ) }
1106- </ CommandList >
1107- </ Command >
1108- </ PopoverContent >
1109- </ Popover >
1120+ ) }
1121+ </ div >
11101122 < Select value = { sharePermission } onValueChange = { ( value : 'viewer' | 'editor' ) => setSharePermission ( value ) } >
11111123 < SelectTrigger className = "w-[130px] font-mono text-sm" >
11121124 < SelectValue />
@@ -1124,15 +1136,19 @@ export function VaultDialog({ open, onOpenChange, vault, initialRequestId, onSav
11241136 type = "button"
11251137 variant = "outline"
11261138 onClick = { handleShareWithUser }
1127- disabled = { saving || ! email . trim ( ) }
1139+ disabled = { saving || ! email . trim ( ) || loadingSuggestions }
11281140 className = "font-mono"
11291141 >
11301142 add
11311143 </ Button >
11321144 </ div >
1133- < p className = "text-xs text-muted-foreground font-mono" >
1134- // { sharePermission === 'viewer' ? 'can_view_publications' : 'can_view_and_edit_publications' }
1135- </ p >
1145+ { shareUserError ? (
1146+ < p className = "text-xs text-destructive font-mono" > { shareUserError } </ p >
1147+ ) : (
1148+ < p className = "text-xs text-muted-foreground font-mono" >
1149+ // choose an existing RefHub user; { sharePermission === 'viewer' ? 'can_view_publications' : 'can_view_and_edit_publications' }
1150+ </ p >
1151+ ) }
11361152 </ div >
11371153
11381154 { shares . length > 0 && (
0 commit comments