mirror of https://github.com/langgenius/dify.git
member selector
This commit is contained in:
parent
6da1a48cad
commit
fcc8789cc3
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue