member selector

This commit is contained in:
JzoNg 2025-08-07 15:05:37 +08:00
parent 6da1a48cad
commit fcc8789cc3
5 changed files with 241 additions and 6 deletions

View File

@ -4,8 +4,9 @@ import { RiCloseLine } from '@remixicon/react'
import Modal from '@/app/components/base/modal'
import Input from '@/app/components/base/input'
import Button from '@/app/components/base/button'
import Recipient from './recipient'
import MailBodyInput from './mail-body-input'
import type { EmailConfig, Recipient } from '../../types'
import type { EmailConfig } from '../../types'
import type {
Node,
NodeOutPutVar,
@ -33,7 +34,7 @@ const EmailConfigureModal = ({
}: EmailConfigureModalProps) => {
const { t } = useTranslation()
const [recipients, setRecipients] = useState<Recipient[]>(config?.recipients || [])
const [recipients, setRecipients] = useState(config?.recipients || { whole_workspace: false, items: [] })
const [subject, setSubject] = useState(config?.subject || '')
const [body, setBody] = useState(config?.body || '')
@ -43,7 +44,7 @@ const EmailConfigureModal = ({
subject,
body,
})
}, [recipients, subject, body, onConfirm])
}, [subject, body, onConfirm])
return (
<Modal
@ -63,6 +64,10 @@ const EmailConfigureModal = ({
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.recipient`)}
</div>
<Recipient
data={recipients}
onChange={setRecipients}
/>
</div>
<div>
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>
@ -91,13 +96,13 @@ const EmailConfigureModal = ({
<Button
variant='primary'
className='w-[72px]'
onClick={onClose}
onClick={handleConfirm}
>
{t('common.operation.save')}
</Button>
<Button
className='w-[72px]'
onClick={handleConfirm}
onClick={onClose}
>
{t('common.operation.cancel')}
</Button>

View File

@ -0,0 +1,78 @@
import { memo } from 'react'
import useSWR from 'swr'
import { useTranslation } from 'react-i18next'
import { RiGroupLine } from '@remixicon/react'
import { useAppContext } from '@/context/app-context'
import Switch from '@/app/components/base/switch'
import MemberSelector from './member-selector'
import { fetchMembers } from '@/service/common'
import type { RecipientData } from '../../../types'
import cn from '@/utils/classnames'
import { produce } from 'immer'
const i18nPrefix = 'workflow.nodes.humanInput'
type Props = {
data: RecipientData
onChange: (data: RecipientData) => void
}
const Recipient = ({
data,
onChange,
}: Props) => {
const { t } = useTranslation()
const { userProfile, currentWorkspace } = useAppContext()
const { data: members } = useSWR(
{
url: '/workspaces/current/members',
params: {},
},
fetchMembers,
)
const accounts = members?.accounts || []
const handleMemberSelect = (id: string) => {
onChange(
produce(data, (draft) => {
draft.items.push({
type: 'member',
user_id: id,
})
}),
)
}
return (
<div className='space-y-1'>
<div className='rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs'>
<div className='flex h-10 items-center justify-between pl-3 pr-1'>
<div className='flex grow items-center gap-2'>
<RiGroupLine className='h-4 w-4 text-text-secondary' />
<div className='system-sm-medium text-text-secondary'>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.memberSelector.title`)}</div>
</div>
<div className='w-[86px]'>
<MemberSelector
value={data.items}
email={userProfile.email}
list={accounts}
onSelect={handleMemberSelect}
/>
</div>
</div>
</div>
<div className='flex h-10 items-center gap-2 rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg pl-2.5 pr-3 shadow-xs'>
<div className='flex h-5 w-5 items-center justify-center rounded-xl bg-components-icon-bg-blue-solid text-[14px]'>
<span className='bg-gradient-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text font-semibold uppercase text-shadow-shadow-1 opacity-90'>{currentWorkspace?.name[0]?.toLocaleUpperCase()}</span>
</div>
<div className={cn('system-sm-medium grow text-text-secondary')}>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.allMembers`, { workspaceName: currentWorkspace.name.replace(/'/g, '') })}</div>
<Switch
defaultValue={data.whole_workspace}
onChange={checked => onChange({ ...data, whole_workspace: checked })}
/>
</div>
</div>
)
}
export default memo(Recipient)

View File

@ -0,0 +1,79 @@
'use client'
import type { FC } from 'react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import Avatar from '@/app/components/base/avatar'
import type { Member } from '@/models/common'
import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.humanInput'
type Props = {
value: any[]
searchValue: string
onSearchChange: (value: string) => void
list: Member[]
onSelect: (value: any) => void
email: string
}
const MemberList: FC<Props> = ({ searchValue, list, value, onSearchChange, onSelect, email }) => {
const { t } = useTranslation()
const filteredList = useMemo(() => {
if (!list.length) return []
if (!searchValue) return list
return list.filter((account) => {
const name = account.name || ''
const email = account.email || ''
return name.toLowerCase().includes(searchValue.toLowerCase())
|| email.toLowerCase().includes(searchValue.toLowerCase())
})
}, [list, searchValue])
return (
<div className='min-w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
<div className='p-2 pb-1'>
<Input
showLeftIcon
value={searchValue}
onChange={e => onSearchChange(e.target.value)}
/>
</div>
<div className='max-h-[248px] overflow-y-auto p-1'>
{filteredList.map(account => (
<div
key={account.id}
className={cn(
'group flex cursor-pointer items-center gap-2 rounded-lg py-1 pl-2 pr-3 hover:bg-state-base-hover',
value.some((item: { user_id: string }) => item.user_id === account.id) && 'bg-transparent hover:bg-transparent',
)}
onClick={() => {
if (value.some((item: { user_id: string }) => item.user_id === account.id)) return
onSelect(account.id)
}}
>
<Avatar className={cn(value.some((item: { user_id: string }) => item.user_id === account.id) && 'opacity-50')} avatar={account.avatar_url} size={24} name={account.name} />
<div className={cn('grow', value.some((item: { user_id: string }) => item.user_id === account.id) && 'opacity-50')}>
<div className='system-sm-medium text-text-secondary'>
{account.name}
{account.status === 'pending' && <span className='system-xs-medium ml-1 text-text-warning'>{t('common.members.pending')}</span>}
{email === account.email && <span className='system-xs-regular text-text-tertiary'>{t('common.members.you')}</span>}
</div>
<div className='system-xs-regular text-text-tertiary'>{account.email}</div>
</div>
{!value.some((item: { user_id: string }) => item.user_id === account.id) && (
<div className='system-xs-medium hidden text-text-accent group-hover:block'>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.memberSelector.add`)}</div>
)}
{value.some((item: { user_id: string }) => item.user_id === account.id) && (
<div className='system-xs-regular text-text-tertiary'>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.memberSelector.added`)}</div>
)}
</div>
))}
</div>
</div>
)
}
export default MemberList

View File

@ -0,0 +1,68 @@
'use client'
import type { FC } from 'react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiContactsBookLine,
} from '@remixicon/react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import Button from '@/app/components/base/button'
import MemberList from './member-list'
import type { Member } from '@/models/common'
import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.humanInput'
type Props = {
value: any[]
email: string
onSelect: (value: any) => void
list: Member[]
}
const MemberSelector: FC<Props> = ({
value,
email,
onSelect,
list = [],
}) => {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [searchValue, setSearchValue] = useState('')
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: 4,
crossAxis: 35,
}}
>
<PortalToFollowElemTrigger
className='w-full'
onClick={() => setOpen(v => !v)}
>
<Button
className={cn('w-full justify-between', open && 'bg-state-accent-hover')}
variant='ghost-accent'
>
<RiContactsBookLine className='mr-1 h-4 w-4' />
<div className=''>{t(`${i18nPrefix}.deliveryMethod.emailConfigure.memberSelector.trigger`)}</div>
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1000]'>
<MemberList
searchValue={searchValue}
list={list}
value={value}
onSearchChange={setSearchValue}
onSelect={onSelect}
email={email}
/>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default MemberSelector

View File

@ -21,8 +21,13 @@ export type Recipient = {
user_id?: string
}
export type RecipientData = {
whole_workspace: boolean
items: Recipient[]
}
export type EmailConfig = {
recipients: Recipient[]
recipients: RecipientData
subject: string
body: string
}