@ -1,8 +1,8 @@
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import React , { use Memo, useState } from 'react'
import React , { use Callback, use Memo, useState } from 'react'
import { useDebounceFn } from 'ahooks'
import { RiArrowDownSLine } from '@remixicon/react'
import { RiArrowDownSLine , RiGroup2Line , RiLock2Line } from '@remixicon/react'
import {
PortalToFollowElem ,
PortalToFollowElemContent ,
@ -10,11 +10,12 @@ import {
} from '@/app/components/base/portal-to-follow-elem'
import Avatar from '@/app/components/base/avatar'
import Input from '@/app/components/base/input'
import { Check } from '@/app/components/base/icons/src/vender/line/general'
import { Users01 , UsersPlus } from '@/app/components/base/icons/src/vender/solid/users'
import { DatasetPermission } from '@/models/datasets'
import { use AppContext } from '@/context/app-context'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import type { Member } from '@/models/common'
import Item from './permission-item'
import MemberItem from './member-item'
export type RoleSelectorProps = {
disabled? : boolean
permission? : DatasetPermission
@ -24,9 +25,16 @@ export type RoleSelectorProps = {
onMemberSelect : ( v : string [ ] ) = > void
}
const PermissionSelector = ( { disabled , permission , value , memberList , onChange , onMemberSelect } : RoleSelectorProps ) = > {
const PermissionSelector = ( {
disabled ,
permission ,
value ,
memberList ,
onChange ,
onMemberSelect ,
} : RoleSelectorProps ) = > {
const { t } = useTranslation ( )
const { userProfile } = useAppContext ( )
const userProfile = useAppContext WithSelector ( state = > state . userProfile )
const [ open , setOpen ] = useState ( false )
const [ keywords , setKeywords ] = useState ( '' )
@ -38,18 +46,18 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
setKeywords ( value )
handleSearch ( )
}
const selectMember = ( member : Member ) = > {
const selectMember = useCallback ( ( member : Member ) = > {
if ( value . includes ( member . id ) )
onMemberSelect ( value . filter ( v = > v !== member . id ) )
else
onMemberSelect ( [ . . . value , member . id ] )
}
} , [ value , onMemberSelect ] )
const selectedMembers = useMemo ( ( ) = > {
return [
userProfile ,
. . . memberList . filter ( member = > member . id !== userProfile . id ) . filter ( member = > value . includes ( member . id ) ) ,
] . map ( member = > member . name ) . join ( ', ' )
]
} , [ userProfile , value , memberList ] )
const showMe = useMemo ( ( ) = > {
@ -60,9 +68,25 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
return memberList . filter ( member = > ( member . name . includes ( searchKeywords ) || member . email . includes ( searchKeywords ) ) && member . id !== userProfile . id && [ 'owner' , 'admin' , 'editor' , 'dataset_operator' ] . includes ( member . role ) )
} , [ memberList , searchKeywords , userProfile ] )
const onSelectOnlyMe = useCallback ( ( ) = > {
onChange ( DatasetPermission . onlyMe )
setOpen ( false )
} , [ onChange ] )
const onSelectAllMembers = useCallback ( ( ) = > {
onChange ( DatasetPermission . allTeamMembers )
setOpen ( false )
} , [ onChange ] )
const onSelectPartialMembers = useCallback ( ( ) = > {
onChange ( DatasetPermission . partialMembers )
onMemberSelect ( [ userProfile . id ] )
} , [ onChange , onMemberSelect , userProfile ] )
const isOnlyMe = permission === DatasetPermission . onlyMe
const isAllTeamMembers = permission === DatasetPermission . allTeamMembers
const isPartialMembers = permission === DatasetPermission . partialMembers
const selectedMemberNames = selectedMembers . map ( member = > member . name ) . join ( ', ' )
return (
< PortalToFollowElem
@ -76,82 +100,122 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
onClick = { ( ) = > ! disabled && setOpen ( v = > ! v ) }
className = 'block'
>
< div className = { cn ( 'flex cursor-pointer items-center rounded-lg bg-components-input-bg-normal px-3 py-[6px] hover:bg-state-base-hover-alt',
< div className = { cn ( 'flex cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal px-2 py-1 hover:bg-state-base-hover-alt',
open && 'bg-state-base-hover-alt' ,
disabled && '!cursor-not-allowed !bg-components-input-bg-disabled hover:!bg-components-input-bg-disabled' ,
) } >
{
isOnlyMe && (
< >
< Avatar avatar = { userProfile . avatar_url } name = { userProfile . name } className = 'mr-2 shrink-0' size = { 24 } / >
< div className = 'mr-2 grow text-sm leading-5 text-components-input-text-filled' > { t ( 'datasetSettings.form.permissionsOnlyMe' ) } < / div >
< div className = 'flex size-6 shrink-0 items-center justify-center' >
< Avatar avatar = { userProfile . avatar_url } name = { userProfile . name } size = { 20 } / >
< / div >
< div className = 'system-sm-regular grow p-1 text-components-input-text-filled' >
{ t ( 'datasetSettings.form.permissionsOnlyMe' ) }
< / div >
< / >
)
}
{
isAllTeamMembers && (
< >
< div className = 'mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]' >
< Users01 className = 'h-3.5 w-3.5 text-[#444CE7]' / >
< div className = 'flex size-6 shrink-0 items-center justify-center' >
< RiGroup2Line className = 'size-4 text-text-secondary' / >
< / div >
< div className = 'system-sm-regular grow p-1 text-components-input-text-filled' >
{ t ( 'datasetSettings.form.permissionsAllMember' ) }
< / div >
< div className = 'mr-2 grow text-sm leading-5 text-components-input-text-filled' > { t ( 'datasetSettings.form.permissionsAllMember' ) } < / div >
< / >
)
}
{
isPartialMembers && (
< >
< div className = 'mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]' >
< Users01 className = 'h-3.5 w-3.5 text-[#444CE7]' / >
< div className = 'relative flex size-6 shrink-0 items-center justify-center' >
{
selectedMembers . length === 1 && (
< Avatar
avatar = { selectedMembers [ 0 ] . avatar_url }
name = { selectedMembers [ 0 ] . name }
size = { 20 }
/ >
)
}
{
selectedMembers . length >= 2 && (
< >
< Avatar
avatar = { selectedMembers [ 0 ] . avatar_url }
name = { selectedMembers [ 0 ] . name }
className = 'absolute left-0 top-0 z-0'
size = { 16 }
/ >
< Avatar
avatar = { selectedMembers [ 1 ] . avatar_url }
name = { selectedMembers [ 1 ] . name }
className = 'absolute bottom-0 right-0 z-10'
size = { 16 }
/ >
< / >
)
}
< / div >
< div
title = { selectedMemberNames }
className = 'system-sm-regular grow truncate p-1 text-components-input-text-filled'
>
{ selectedMemberNames }
< / div >
< div title = { selectedMembers } className = 'mr-2 grow truncate text-sm leading-5 text-components-input-text-filled' > { selectedMembers } < / div >
< / >
)
}
< RiArrowDownSLine className = { cn ( 'h-4 w-4 shrink-0 text-text-secondary' , disabled && '!text-components-input-text-placeholder' ) } / >
< RiArrowDownSLine
className = { cn (
'h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary' ,
open && 'text-text-secondary' ,
disabled && '!text-components-input-text-placeholder' ,
) }
/ >
< / div >
< / PortalToFollowElemTrigger >
< PortalToFollowElemContent className = 'z-[1002]' >
< div className = 'relative w-[480px] rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm' >
< div className = 'relative w-[480px] rounded- x l border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg shadow-shadow-shadow-5 '>
< div className = 'p-1' >
< div className = 'cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick = { ( ) = > {
onChange ( DatasetPermission . onlyMe )
setOpen ( false )
} } >
< div className = 'flex items-center gap-2' >
< Avatar avatar = { userProfile . avatar_url } name = { userProfile . name } className = 'mr-2 shrink-0' size = { 24 } / >
< div className = 'mr-2 grow text-sm leading-5 text-text-primary' > { t ( 'datasetSettings.form.permissionsOnlyMe' ) } < / div >
{ isOnlyMe && < Check className = 'h-4 w-4 text-primary-600' / > }
< / div >
< / div >
< div className = 'cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick = { ( ) = > {
onChange ( DatasetPermission . allTeamMembers )
setOpen ( false )
} } >
< div className = 'flex items-center gap-2' >
< div className = 'mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#EEF4FF]' >
< Users01 className = 'h-3.5 w-3.5 text-[#444CE7]' / >
{ /* Only me */ }
< Item
leftIcon = {
< Avatar avatar = { userProfile . avatar_url } name = { userProfile . name } className = 'shrink-0' size = { 24 } / >
}
text = { t ( 'datasetSettings.form.permissionsOnlyMe' ) }
onClick = { onSelectOnlyMe }
isSelected = { isOnlyMe }
/ >
{ /* All team members */ }
< Item
leftIcon = {
< div className = 'flex size-6 shrink-0 items-center justify-center' >
< RiGroup2Line className = 'size-4 text-text-secondary' / >
< / div >
< div className = 'mr-2 grow text-sm leading-5 text-text-primary' > { t ( 'datasetSettings.form.permissionsAllMember' ) } < / div >
{ isAllTeamMembers && < Check className = 'h-4 w-4 text-primary-600' / > }
< / div >
< / div >
< div className = 'cursor-pointer rounded-lg py-1 pl-3 pr-2 hover:bg-state-base-hover' onClick = { ( ) = > {
onChange ( DatasetPermission . partialMembers )
onMemberSelect ( [ userProfile . id ] )
} } >
< div className = 'flex items-center gap-2' >
< div className = { cn ( 'mr-2 flex h-6 w-6 items-center justify-center rounded-lg bg-[#FFF6ED]' , isPartialMembers && '!bg-[#EEF4FF]' ) } >
< UsersPlus className = { cn ( 'h-3.5 w-3.5 text-[#FB6514]' , isPartialMembers && '!text-[#444CE7]' ) } / >
}
text = { t ( 'datasetSettings.form.permissionsAllMember' ) }
onClick = { onSelectAllMembers }
isSelected = { isAllTeamMembers }
/ >
{ /* Partial members */ }
< Item
leftIcon = {
< div className = 'flex size-6 shrink-0 items-center justify-center' >
< RiLock2Line className = 'size-4 text-text-secondary' / >
< / div >
< div className = 'mr-2 grow text-sm leading-5 text-text-primary' > { t ( 'datasetSettings.form.permissionsInvitedMembers' ) } < / div >
{ isPartialMembers && < Check className = 'h-4 w-4 text-primary-600' / > }
< / div >
< / div >
}
text = { t ( 'datasetSettings.form.permissionsInvitedMembers' ) }
onClick = { onSelectPartialMembers }
isSelected = { isPartialMembers }
/ >
< / div >
{ isPartialMembers && (
< div className = 'max-h-[360px] overflow-y-auto border-t-[1px] border-divider-regular pb-1 pl-1 pr-1 '>
< div className = 'sticky left-0 top-0 z-10 bg-white p-2 pb-1'>
< div className = 'max-h-[360px] overflow-y-auto border-t-[1px] border-divider-regular '>
< div className = 'sticky left-0 top-0 z-10 p-2 pb-1'>
< Input
showLeftIcon
showClearIcon
@ -160,29 +224,37 @@ const PermissionSelector = ({ disabled, permission, value, memberList, onChange,
onClear = { ( ) = > handleKeywordsChange ( '' ) }
/ >
< / div >
{ showMe && (
< div className = 'flex items-center gap-2 rounded-lg py-1 pl-3 pr-[10px]' >
< Avatar avatar = { userProfile . avatar_url } name = { userProfile . name } className = 'shrink-0' size = { 24 } / >
< div className = 'grow' >
< div className = 'truncate text-[13px] font-medium leading-[18px] text-text-secondary' >
{ userProfile . name }
< span className = 'text-xs font-normal text-text-tertiary' > { t ( 'datasetSettings.form.me' ) } < / span >
< div className = 'flex flex-col p-1' >
{ showMe && (
< MemberItem
leftIcon = {
< Avatar avatar = { userProfile . avatar_url } name = { userProfile . name } className = 'shrink-0' size = { 24 } / >
}
name = { userProfile . name }
email = { userProfile . email }
isSelected
isMe
/ >
) }
{ filteredMemberList . map ( member = > (
< MemberItem
leftIcon = {
< Avatar avatar = { member . avatar_url } name = { member . name } className = 'shrink-0' size = { 24 } / >
}
name = { member . name }
email = { member . email }
isSelected = { value . includes ( member . id ) }
onClick = { selectMember . bind ( null , member ) }
/ >
) ) }
{
! showMe && filteredMemberList . length === 0 && (
< div className = 'system-xs-regular flex items-center justify-center whitespace-pre-wrap px-1 py-6 text-center text-text-tertiary' >
{ t ( 'datasetSettings.form.onSearchResults' ) }
< / div >
< div className = 'truncate text-xs leading-[18px] text-text-tertiary' > { userProfile . email } < / div >
< / div >
< Check className = 'h-4 w-4 shrink-0 text-text-accent opacity-30' / >
< / div >
) }
{ filteredMemberList . map ( member = > (
< div key = { member . id } className = 'flex cursor-pointer items-center gap-2 rounded-lg py-1 pl-3 pr-[10px] hover:bg-state-base-hover' onClick = { ( ) = > selectMember ( member ) } >
< Avatar avatar = { userProfile . avatar_url } name = { member . name } className = 'shrink-0' size = { 24 } / >
< div className = 'grow' >
< div className = 'truncate text-[13px] font-medium leading-[18px] text-text-secondary' > { member . name } < / div >
< div className = 'truncate text-xs leading-[18px] text-text-tertiary' > { member . email } < / div >
< / div >
{ value . includes ( member . id ) && < Check className = 'h-4 w-4 shrink-0 text-text-accent' / > }
< / div >
) ) }
)
}
< / div >
< / div >
) }
< / div >