mirror of
https://github.com/langgenius/dify.git
synced 2026-04-27 19:27:23 +08:00
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 Modal from '@/app/components/base/modal'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
|
import Recipient from './recipient'
|
||||||
import MailBodyInput from './mail-body-input'
|
import MailBodyInput from './mail-body-input'
|
||||||
import type { EmailConfig, Recipient } from '../../types'
|
import type { EmailConfig } from '../../types'
|
||||||
import type {
|
import type {
|
||||||
Node,
|
Node,
|
||||||
NodeOutPutVar,
|
NodeOutPutVar,
|
||||||
@ -33,7 +34,7 @@ const EmailConfigureModal = ({
|
|||||||
}: EmailConfigureModalProps) => {
|
}: EmailConfigureModalProps) => {
|
||||||
const { t } = useTranslation()
|
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 [subject, setSubject] = useState(config?.subject || '')
|
||||||
const [body, setBody] = useState(config?.body || '')
|
const [body, setBody] = useState(config?.body || '')
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ const EmailConfigureModal = ({
|
|||||||
subject,
|
subject,
|
||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
}, [recipients, subject, body, onConfirm])
|
}, [subject, body, onConfirm])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@ -63,6 +64,10 @@ const EmailConfigureModal = ({
|
|||||||
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>
|
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>
|
||||||
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.recipient`)}
|
{t(`${i18nPrefix}.deliveryMethod.emailConfigure.recipient`)}
|
||||||
</div>
|
</div>
|
||||||
|
<Recipient
|
||||||
|
data={recipients}
|
||||||
|
onChange={setRecipients}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>
|
<div className='system-sm-medium mb-1 flex h-6 items-center text-text-secondary'>
|
||||||
@ -91,13 +96,13 @@ const EmailConfigureModal = ({
|
|||||||
<Button
|
<Button
|
||||||
variant='primary'
|
variant='primary'
|
||||||
className='w-[72px]'
|
className='w-[72px]'
|
||||||
onClick={onClose}
|
onClick={handleConfirm}
|
||||||
>
|
>
|
||||||
{t('common.operation.save')}
|
{t('common.operation.save')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className='w-[72px]'
|
className='w-[72px]'
|
||||||
onClick={handleConfirm}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
{t('common.operation.cancel')}
|
{t('common.operation.cancel')}
|
||||||
</Button>
|
</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
|
user_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RecipientData = {
|
||||||
|
whole_workspace: boolean
|
||||||
|
items: Recipient[]
|
||||||
|
}
|
||||||
|
|
||||||
export type EmailConfig = {
|
export type EmailConfig = {
|
||||||
recipients: Recipient[]
|
recipients: RecipientData
|
||||||
subject: string
|
subject: string
|
||||||
body: string
|
body: string
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user