mirror of https://github.com/langgenius/dify.git
email sender modal
This commit is contained in:
parent
527736b8e4
commit
463ea14d44
|
|
@ -83,7 +83,7 @@ const EmailConfigureModal = ({
|
|||
body,
|
||||
debug: debugMode,
|
||||
})
|
||||
}, [subject, body, onConfirm])
|
||||
}, [recipients, subject, body, debugMode, onConfirm])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import type {
|
|||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import TestEmailSender from './test-email-sender'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.humanInput'
|
||||
|
||||
|
|
@ -41,6 +42,7 @@ const DeliveryMethodItem: React.FC<Props> = ({
|
|||
const { t } = useTranslation()
|
||||
const [isHovering, setIsHovering] = React.useState(false)
|
||||
const [showEmailModal, setShowEmailModal] = React.useState(false)
|
||||
const [showTestEmailModal, setShowTestEmailModal] = React.useState(false)
|
||||
|
||||
const handleEnableStatusChange = (enabled: boolean) => {
|
||||
onChange({
|
||||
|
|
@ -73,13 +75,13 @@ const DeliveryMethodItem: React.FC<Props> = ({
|
|||
</div>
|
||||
)}
|
||||
<div className='system-xs-medium capitalize text-text-secondary'>{method.type}</div>
|
||||
{method.type === DeliveryMethodType.Email && method.config?.debug && <Badge size='s'>DEBUG</Badge>}
|
||||
{method.type === DeliveryMethodType.Email && method.config?.debug && <Badge size='s' className='!px-1 !py-0.5'>DEBUG</Badge>}
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className='hidden items-end gap-1 group-hover:flex'>
|
||||
{method.type === DeliveryMethodType.Email && method.config && (
|
||||
<>
|
||||
<ActionButton onClick={() => setShowEmailModal(true)}>
|
||||
<ActionButton onClick={() => setShowTestEmailModal(true)}>
|
||||
<RiSendPlane2Line className='h-4 w-4' />
|
||||
</ActionButton>
|
||||
<ActionButton onClick={() => setShowEmailModal(true)}>
|
||||
|
|
@ -130,6 +132,19 @@ const DeliveryMethodItem: React.FC<Props> = ({
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{showTestEmailModal && (
|
||||
<TestEmailSender
|
||||
isShow={showTestEmailModal}
|
||||
config={method.config}
|
||||
nodesOutputVars={nodesOutputVars}
|
||||
availableNodes={availableNodes}
|
||||
onClose={() => setShowTestEmailModal(false)}
|
||||
onConfirm={(data) => {
|
||||
handleConfigChange(data)
|
||||
setShowTestEmailModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type Props = {
|
|||
onDelete: (recipient: RecipientItem) => void
|
||||
onSelect: (value: any) => void
|
||||
onAdd: (email: string) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const EmailInput = ({
|
||||
|
|
@ -29,6 +30,7 @@ const EmailInput = ({
|
|||
onDelete,
|
||||
onSelect,
|
||||
onAdd,
|
||||
disabled = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
|
@ -50,6 +52,7 @@ const EmailInput = ({
|
|||
}, [selectedEmails, t, isFocus])
|
||||
|
||||
const setInputFocus = () => {
|
||||
if (disabled) return
|
||||
setIsFocus(true)
|
||||
const input = inputRef.current?.children[0] as HTMLInputElement
|
||||
input?.focus()
|
||||
|
|
@ -104,7 +107,11 @@ const EmailInput = ({
|
|||
return (
|
||||
<div className='p-1 pt-0'>
|
||||
<div
|
||||
className={cn('flex max-h-24 min-h-16 flex-wrap overflow-y-auto rounded-lg border border-transparent bg-components-input-bg-normal p-2 hover:border-components-input-border-hover hover:bg-components-input-bg-hover', isFocus && 'border-components-input-border-active bg-components-input-bg-active shadow-xs')}
|
||||
className={cn(
|
||||
'flex max-h-24 min-h-16 flex-wrap overflow-y-auto rounded-lg border border-transparent bg-components-input-bg-normal p-2',
|
||||
isFocus && 'border-components-input-border-active bg-components-input-bg-active shadow-xs',
|
||||
!disabled && 'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
|
||||
)}
|
||||
onClick={setInputFocus}
|
||||
>
|
||||
{selectedEmails.map(item => (
|
||||
|
|
@ -113,41 +120,44 @@ const EmailInput = ({
|
|||
email={email}
|
||||
data={item as unknown as Member}
|
||||
onDelete={onDelete}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: -40,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger className='block h-6 min-w-[166px]'>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className='system-sm-regular h-6 min-w-[166px] appearance-none bg-transparent p-1 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder'
|
||||
placeholder={placeholder}
|
||||
onFocus={() => setIsFocus(true)}
|
||||
onBlur={() => setIsFocus(false)}
|
||||
value={searchKey}
|
||||
onChange={handleValueChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<MemberList
|
||||
searchValue={searchKey}
|
||||
list={list}
|
||||
value={value}
|
||||
onSearchChange={setSearchKey}
|
||||
onSelect={handleSelect}
|
||||
email={email}
|
||||
hideSearch
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
{!disabled && (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: -40,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger className='block h-6 min-w-[166px]'>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className='system-sm-regular h-6 min-w-[166px] appearance-none bg-transparent p-1 text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder'
|
||||
placeholder={placeholder}
|
||||
onFocus={() => setIsFocus(true)}
|
||||
onBlur={() => setIsFocus(false)}
|
||||
value={searchKey}
|
||||
onChange={handleValueChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<MemberList
|
||||
searchValue={searchKey}
|
||||
list={list}
|
||||
value={value}
|
||||
onSearchChange={setSearchKey}
|
||||
onSelect={handleSelect}
|
||||
email={email}
|
||||
hideSearch
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import Avatar from '@/app/components/base/avatar'
|
|||
type Props = {
|
||||
email: string
|
||||
data: Member
|
||||
disabled?: boolean
|
||||
onDelete: (recipient: RecipientItem) => void
|
||||
|
||||
}
|
||||
|
|
@ -16,6 +17,7 @@ const EmailItem = ({
|
|||
email,
|
||||
data,
|
||||
onDelete,
|
||||
disabled = false,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
|
|
@ -29,10 +31,12 @@ const EmailItem = ({
|
|||
{email === data.email ? data.name : data.email}
|
||||
{email === data.email && <span className='system-xs-regular text-text-tertiary'>{t('common.members.you')}</span>}
|
||||
</div>
|
||||
<RiCloseCircleFill
|
||||
className='h-4 w-4 cursor-pointer text-text-quaternary hover:text-text-tertiary'
|
||||
onClick={() => onDelete(data as unknown as RecipientItem)}
|
||||
/>
|
||||
{!disabled && (
|
||||
<RiCloseCircleFill
|
||||
className='h-4 w-4 cursor-pointer text-text-quaternary hover:text-text-tertiary'
|
||||
onClick={() => onDelete(data as unknown as RecipientItem)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,230 @@
|
|||
import { memo, useCallback, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { RiArrowRightSFill, RiCloseLine } from '@remixicon/react'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import EmailInput from './recipient/email-input'
|
||||
import type { EmailConfig } from '../../types'
|
||||
import type {
|
||||
Node,
|
||||
NodeOutPutVar,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { fetchMembers } from '@/service/common'
|
||||
import { noop } from 'lodash-es'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.humanInput'
|
||||
|
||||
type EmailConfigureModalProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onConfirm: (data: any) => void
|
||||
config?: EmailConfig
|
||||
nodesOutputVars?: NodeOutPutVar[]
|
||||
availableNodes?: Node[]
|
||||
}
|
||||
|
||||
const EmailSenderModal = ({
|
||||
isShow,
|
||||
onClose,
|
||||
onConfirm,
|
||||
config,
|
||||
nodesOutputVars = [],
|
||||
availableNodes = [],
|
||||
}: EmailConfigureModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { userProfile, currentWorkspace } = useAppContext()
|
||||
|
||||
const debugEnabled = !!config?.debug
|
||||
const onlyWholeTeam = config?.recipients?.whole_workspace && (!config?.recipients?.items || config?.recipients?.items.length === 0)
|
||||
const onlySpecificUsers = !config?.recipients?.whole_workspace && config?.recipients?.items && config?.recipients?.items.length > 0
|
||||
const combinedRecipients = config?.recipients?.whole_workspace && config?.recipients?.items && config?.recipients?.items.length > 0
|
||||
|
||||
const { data: members } = useSWR(
|
||||
{
|
||||
url: '/workspaces/current/members',
|
||||
params: {},
|
||||
},
|
||||
fetchMembers,
|
||||
)
|
||||
const accounts = members?.accounts || []
|
||||
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
const [done, setDone] = useState(false)
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
// TODO send api
|
||||
setDone(true)
|
||||
}, [onConfirm])
|
||||
|
||||
if (done) {
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={noop}
|
||||
className='relative !max-w-[480px] !p-0'
|
||||
>
|
||||
<div className='space-y-2 p-6 pb-3'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t(`${i18nPrefix}.deliveryMethod.emailSender.done`)}</div>
|
||||
{debugEnabled && (
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugDone`}
|
||||
components={{ email: <span className='system-md-semibold text-text-secondary'></span> }}
|
||||
values={{ email: userProfile.email }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!debugEnabled && onlyWholeTeam && (
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone2`}
|
||||
components={{ team: <span className='system-md-medium text-text-secondary'></span> }}
|
||||
values={{ team: currentWorkspace.name.replace(/'/g, '’') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!debugEnabled && onlySpecificUsers && (
|
||||
<div className='system-md-regular text-text-secondary'>{t(`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone3`)}</div>
|
||||
)}
|
||||
{!debugEnabled && combinedRecipients && (
|
||||
<div className='system-md-regular text-text-secondary'>
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamDone1`}
|
||||
components={{ team: <span className='system-md-medium text-text-secondary'></span> }}
|
||||
values={{ team: currentWorkspace.name.replace(/'/g, '’') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(onlySpecificUsers || combinedRecipients) && (
|
||||
<div className='px-5'>
|
||||
<EmailInput
|
||||
disabled
|
||||
email={userProfile.email}
|
||||
value={config?.recipients?.items}
|
||||
list={accounts}
|
||||
onDelete={noop}
|
||||
onSelect={noop}
|
||||
onAdd={noop}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex flex-row-reverse gap-2 p-6 pt-5'>
|
||||
<Button
|
||||
variant='primary'
|
||||
className='w-[72px]'
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.operation.ok')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={noop}
|
||||
className='relative !max-w-[480px] !p-0'
|
||||
>
|
||||
<div className='absolute right-5 top-5 cursor-pointer p-1.5' onClick={onClose}>
|
||||
<RiCloseLine className='h-5 w-5 text-text-tertiary' />
|
||||
</div>
|
||||
<div className='space-y-1 p-6 pb-3'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t(`${i18nPrefix}.deliveryMethod.emailSender.title`)}</div>
|
||||
{debugEnabled && (
|
||||
<>
|
||||
<div className='system-sm-regular text-text-secondary'>{t(`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip`)}</div>
|
||||
<div className='system-sm-regular text-text-secondary'>
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.debugModeTip2`}
|
||||
components={{ email: <span className='system-sm-semibold text-text-primary'></span> }}
|
||||
values={{ email: userProfile.email }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{!debugEnabled && onlyWholeTeam && (
|
||||
<div className='system-sm-regular text-text-secondary'>
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip2`}
|
||||
components={{ team: <span className='system-sm-semibold text-text-primary'></span> }}
|
||||
values={{ team: currentWorkspace.name.replace(/'/g, '’') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!debugEnabled && onlySpecificUsers && (
|
||||
<div className='system-sm-regular text-text-secondary'>{t(`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip3`)}</div>
|
||||
)}
|
||||
{!debugEnabled && combinedRecipients && (
|
||||
<div className='system-sm-regular text-text-secondary'>
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.wholeTeamTip1`}
|
||||
components={{ team: <span className='system-sm-semibold text-text-primary'></span> }}
|
||||
values={{ team: currentWorkspace.name.replace(/'/g, '’') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(onlySpecificUsers || combinedRecipients) && (
|
||||
<>
|
||||
<div className='px-5'>
|
||||
<EmailInput
|
||||
disabled
|
||||
email={userProfile.email}
|
||||
value={config?.recipients?.items}
|
||||
list={accounts}
|
||||
onDelete={noop}
|
||||
onSelect={noop}
|
||||
onAdd={noop}
|
||||
/>
|
||||
</div>
|
||||
<div className='system-xs-regular px-6 pt-1 text-text-tertiary'>
|
||||
<Trans
|
||||
i18nKey={`${i18nPrefix}.deliveryMethod.emailSender.tip`}
|
||||
components={{ strong: <span className='system-xs-regular text-text-accent'></span> }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* vars */}
|
||||
<div className='px-6'>
|
||||
<Divider className='!mb-2 !mt-4 !h-px !w-12 bg-divider-regular'/>
|
||||
</div>
|
||||
<div className='px-6 py-2'>
|
||||
<div className='group flex h-6 cursor-pointer items-center' onClick={() => setCollapsed(!collapsed)}>
|
||||
<div className='system-sm-semibold-uppercase mr-1 text-text-secondary'>{t(`${i18nPrefix}.deliveryMethod.emailSender.vars`)}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{t(`${i18nPrefix}.deliveryMethod.emailSender.optional`)}</div>
|
||||
<RiArrowRightSFill className={cn('h-4 w-4 text-text-quaternary group-hover:text-text-primary', !collapsed && 'rotate-90')} />
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{t(`${i18nPrefix}.deliveryMethod.emailSender.varsTip`)}</div>
|
||||
{!collapsed && (
|
||||
<div className='mt-3'>
|
||||
{/* form TODO */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex flex-row-reverse gap-2 p-6 pt-5'>
|
||||
<Button
|
||||
variant='primary'
|
||||
onClick={handleConfirm}
|
||||
>
|
||||
{t(`${i18nPrefix}.deliveryMethod.emailSender.send`)}
|
||||
</Button>
|
||||
<Button
|
||||
className='w-[72px]'
|
||||
onClick={onClose}
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(EmailSenderModal)
|
||||
|
|
@ -963,6 +963,24 @@ const translation = {
|
|||
debugModeTip1: 'During debug mode, emails will only be sent to your account email <email>{{email}}</email>.',
|
||||
debugModeTip2: 'The production environment is not affected.',
|
||||
},
|
||||
emailSender: {
|
||||
title: 'Test Email Sender',
|
||||
debugModeTip: 'Debug mode is enabled.',
|
||||
debugModeTip2: 'Email will be sent to <email>{{email}}</email>.',
|
||||
wholeTeamTip1: 'Email will be sent to <team>{{team}}</team> members and the following email addresses:',
|
||||
wholeTeamTip2: 'Email will be sent to <team>{{team}}</team> members.',
|
||||
wholeTeamTip3: 'Email will be sent to the following email addresses:',
|
||||
tip: 'It is recommended to <strong>enable Debug Mode</strong> for testing email delivery.',
|
||||
vars: 'Variables in Form Content',
|
||||
optional: '(optional)',
|
||||
varsTip: 'Fill in form variables to emulate what recipients actually see.',
|
||||
send: 'Send Email',
|
||||
done: 'Email Sent',
|
||||
debugDone: 'A test email has been sent to <email>{{email}}</email>. Please check your inbox.',
|
||||
wholeTeamDone1: 'Email will be sent to <team>{{team}}</team> members and the following email addresses:',
|
||||
wholeTeamDone2: 'Email will be sent to <team>{{team}}</team> members.',
|
||||
wholeTeamDone3: 'Email will be sent to the following email addresses:',
|
||||
},
|
||||
},
|
||||
formContent: {
|
||||
title: 'Form Content',
|
||||
|
|
|
|||
|
|
@ -963,6 +963,24 @@ const translation = {
|
|||
debugModeTip1: '在调试模式下,电子邮件将仅发送到您的帐户电子邮件 <email>{{email}}</email>。',
|
||||
debugModeTip2: '生产环境不受影响。',
|
||||
},
|
||||
emailSender: {
|
||||
title: '测试邮件发送器',
|
||||
debugModeTip: '调试模式已启用。',
|
||||
debugModeTip2: '邮件将发送到 <email>{{email}}</email>。',
|
||||
wholeTeamTip1: '邮件将发送给 <team>{{team}}</team> 成员和以下邮件地址:',
|
||||
wholeTeamTip2: '邮件将发送给 <team>{{team}}</team> 成员。',
|
||||
wholeTeamTip3: '邮件将发送到以下邮件地址:',
|
||||
tip: '建议为测试邮件发送启用 <strong>调试模式</strong>。',
|
||||
vars: '表单内容中的变量',
|
||||
optional: '(可选)',
|
||||
varsTip: '填写表单变量以模拟收件人实际看到的内容。',
|
||||
send: '发送邮件',
|
||||
done: '邮件已发送',
|
||||
debugDone: '测试邮件已发送到 <email>{{email}}</email>。请检查您的收件箱。',
|
||||
wholeTeamDone1: '邮件将发送给 <team>{{team}}</team> 成员和以下邮件地址:',
|
||||
wholeTeamDone2: '邮件将发送给 <team>{{team}}</team> 成员。',
|
||||
wholeTeamDone3: '邮件将发送到以下邮件地址:',
|
||||
},
|
||||
},
|
||||
formContent: {
|
||||
title: '表单内容',
|
||||
|
|
|
|||
Loading…
Reference in New Issue