Skip to content

Commit 2e9fd44

Browse files
committed
feat: send mail content
1 parent 53b89a6 commit 2e9fd44

22 files changed

+1418
-87
lines changed

client/package-lock.json

Lines changed: 701 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@
4949
"@tanstack/react-router": "^1.16.6",
5050
"@tanstack/react-table": "^8.12.0",
5151
"@tanstack/router-devtools": "^1.16.6",
52+
"@tiptap/extension-color": "^2.2.4",
53+
"@tiptap/extension-list-item": "^2.2.4",
54+
"@tiptap/extension-text-align": "^2.2.4",
55+
"@tiptap/extension-text-style": "^2.2.4",
56+
"@tiptap/extension-underline": "^2.2.4",
57+
"@tiptap/react": "^2.2.4",
58+
"@tiptap/starter-kit": "^2.2.4",
5259
"@xyflow/react": "^12.0.0-next.11",
5360
"axios": "^1.6.7",
5461
"class-variance-authority": "^0.7.0",

client/src/components/pages/flow-detail/constant.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
HttpRequestDialogContent,
1717
MessageDialogContent,
1818
PromptAndCollectDialogContent,
19+
SendMailContent,
1920
SubFlowContent,
2021
} from './node-dialog'
2122

@@ -42,6 +43,11 @@ export const MAP_ACTION_TO_LABEL: Record<EActionTypes, string> = {
4243
) as string,
4344
}
4445

46+
export const ACTIONS_TO_RENDER_LANG = [
47+
EActionTypes.MESSAGE,
48+
EActionTypes.PROMPT_AND_COLLECT,
49+
]
50+
4551
export const useMapActionToLabel = () => {
4652
const { t } = useTranslation('flowDetail')
4753

@@ -91,7 +97,7 @@ export const MAP_ACTION: Record<
9197
[EActionTypes.SEND_MAIL]: {
9298
icon: () => <Mail className='w-4 h-4' />,
9399
label: 'Send mail',
94-
dialogContent: () => <div>Send mail</div>,
100+
dialogContent: () => <SendMailContent />,
95101
},
96102
[EActionTypes.FALLBACK]: {
97103
icon: () => <Webhook className='w-4 h-4' />,

client/src/components/pages/flow-detail/flow-inside.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
import {
2+
AlertDialog,
3+
AlertDialogAction,
4+
AlertDialogCancel,
5+
AlertDialogContent,
6+
AlertDialogDescription,
7+
AlertDialogFooter,
8+
AlertDialogHeader,
9+
AlertDialogTitle,
10+
AlertDialogTrigger,
11+
} from '@/components/ui'
12+
import { useTranslation } from 'react-i18next'
13+
import { useBlocker } from 'react-router-dom'
114
import ReactFlow, { Background, BackgroundVariant } from 'reactflow'
215
import { Actions } from './actions'
316
import { Controls } from './controls'
@@ -19,10 +32,49 @@ export const FlowInside = () => {
1932
handleInit,
2033
handleDoubleClickNode,
2134
handleDoubleClickEdge,
35+
flow,
2236
} = useFlowCtx()
37+
const { t } = useTranslation('common')
38+
39+
const blocker = useBlocker(({ currentLocation, nextLocation }) => {
40+
return (
41+
(JSON.stringify(nodes) !== JSON.stringify(flow.nodes) ||
42+
JSON.stringify(edges) !== JSON.stringify(flow.edges)) &&
43+
currentLocation.pathname !== nextLocation.pathname
44+
)
45+
})
2346

2447
return (
2548
<div className='h-svh select-none'>
49+
<AlertDialog open={blocker.state === 'blocked'}>
50+
<AlertDialogTrigger asChild>
51+
<div className='hidden'></div>
52+
</AlertDialogTrigger>
53+
<AlertDialogContent>
54+
<AlertDialogHeader>
55+
<AlertDialogTitle>{t('leave_page_unsaved.title')}</AlertDialogTitle>
56+
<AlertDialogDescription>
57+
{t('leave_page_unsaved.desc')}
58+
</AlertDialogDescription>
59+
</AlertDialogHeader>
60+
<AlertDialogFooter>
61+
<AlertDialogCancel
62+
onClick={() => {
63+
blocker.reset?.()
64+
}}
65+
>
66+
{t('cancel')}
67+
</AlertDialogCancel>
68+
<AlertDialogAction
69+
onClick={() => {
70+
blocker.proceed?.()
71+
}}
72+
>
73+
{t('confirm')}
74+
</AlertDialogAction>
75+
</AlertDialogFooter>
76+
</AlertDialogContent>
77+
</AlertDialog>
2678
<ReactFlow
2779
nodeTypes={nodeTypes}
2880
nodes={nodes}

client/src/components/pages/flow-detail/flow-provider.tsx

Lines changed: 47 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -58,45 +58,53 @@ type Props = {
5858
export const FlowProvider = ({ children, flow }: Props) => {
5959
const [open, toggle] = useToggle()
6060
const actionToLabel = useMapActionToLabel()
61-
const [nodes, setNodes, onNodesChange] = useNodesState<any>([
62-
{
63-
id: EActionTypes.START,
64-
type: EActionTypes.START,
65-
position: { x: 100, y: 100 },
66-
data: {
67-
label: 'Start',
68-
action: EActionTypes.START,
69-
id: EActionTypes.START,
70-
name: 'Start',
71-
},
72-
deletable: false,
73-
draggable: false,
74-
},
75-
{
76-
id: EActionTypes.FALLBACK,
77-
type: EActionTypes.FALLBACK,
78-
position: { x: 190, y: 280 },
79-
data: {
80-
label: 'Fallback',
81-
id: EActionTypes.FALLBACK,
82-
action: EActionTypes.FALLBACK,
83-
name: 'Fallback',
84-
},
85-
deletable: false,
86-
draggable: false,
87-
},
88-
])
89-
const [edges, setEdges, onEdgesChange] = useEdgesState([
90-
{
91-
id: 'start-fallback',
92-
source: EActionTypes.START,
93-
target: EActionTypes.FALLBACK,
94-
type: 'custom',
95-
data: {
96-
deletable: false,
97-
},
98-
},
99-
])
61+
const [nodes, setNodes, onNodesChange] = useNodesState<any>(
62+
flow.nodes?.length
63+
? (flow.nodes as Node<any>[])
64+
: [
65+
{
66+
id: EActionTypes.START,
67+
type: EActionTypes.START,
68+
position: { x: 100, y: 100 },
69+
data: {
70+
label: 'Start',
71+
action: EActionTypes.START,
72+
id: EActionTypes.START,
73+
name: 'Start',
74+
},
75+
deletable: false,
76+
draggable: false,
77+
},
78+
{
79+
id: EActionTypes.FALLBACK,
80+
type: EActionTypes.FALLBACK,
81+
position: { x: 190, y: 280 },
82+
data: {
83+
label: 'Fallback',
84+
id: EActionTypes.FALLBACK,
85+
action: EActionTypes.FALLBACK,
86+
name: 'Fallback',
87+
},
88+
deletable: false,
89+
draggable: false,
90+
},
91+
],
92+
)
93+
const [edges, setEdges, onEdgesChange] = useEdgesState(
94+
flow.edges?.length
95+
? (flow.edges as Edge<any>[])
96+
: [
97+
{
98+
id: 'start-fallback',
99+
source: EActionTypes.START,
100+
target: EActionTypes.FALLBACK,
101+
type: 'custom',
102+
data: {
103+
deletable: false,
104+
},
105+
},
106+
],
107+
)
100108
const [selectedNode, setSelectedNode] = useState<Node<any> | null>(null)
101109
const [_selectedEdge, setSelectedEdge] = useState<Edge<any> | null>(null)
102110
const [currentLang, setCurrentLang] = useState(

client/src/components/pages/flow-detail/node-dialog/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ export * from './http-request'
33
export * from './message'
44
export * from './node-dialog'
55
export * from './prompt-and-collect'
6+
export * from './send-mail'
67
export * from './subflow'

client/src/components/pages/flow-detail/node-dialog/node-dialog.tsx

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import { EActionTypes } from '@/types/flow'
1919
import { useEffect, useState } from 'react'
2020
import { useTranslation } from 'react-i18next'
2121
import { useDebounceValue } from 'usehooks-ts'
22-
import { useFlowCtx } from '..'
23-
import { MAP_ACTION } from '../constant'
22+
import { ACTIONS_TO_RENDER_LANG, MAP_ACTION } from '../constant'
23+
import { useFlowCtx } from '../flow-provider'
2424

2525
export const NodeDialog = () => {
2626
const { t } = useTranslation(['flowDetail', 'forms'])
@@ -83,34 +83,32 @@ export const NodeDialog = () => {
8383
placeholder={t('forms:name.placeholder')}
8484
/>
8585
</div>
86-
{selectedNode.data.action !== EActionTypes.CHECK_VARIABLES &&
87-
selectedNode.data.action !== EActionTypes.HTTP_REQUEST &&
88-
selectedNode.data.action !== EActionTypes.SUB_FLOW && (
89-
<div className='space-y-2'>
90-
<Label>{t('forms:bot_lang.label')}</Label>
91-
<Select
92-
onValueChange={(value) => {
93-
handleChangeLang(value)
94-
}}
95-
value={currentLang}
96-
>
97-
<SelectTrigger>
98-
<SelectValue
99-
placeholder={t('forms:bot_lang.placeholder')}
100-
/>
101-
</SelectTrigger>
102-
<SelectContent>
103-
{Object.keys(LANGS).map((lang) => {
104-
return (
105-
<SelectItem key={lang} value={lang}>
106-
{LANGS[lang]}
107-
</SelectItem>
108-
)
109-
})}
110-
</SelectContent>
111-
</Select>
112-
</div>
113-
)}
86+
{ACTIONS_TO_RENDER_LANG.includes(
87+
selectedNode.data.action as unknown as EActionTypes,
88+
) && (
89+
<div className='space-y-2'>
90+
<Label>{t('forms:bot_lang.label')}</Label>
91+
<Select
92+
onValueChange={(value) => {
93+
handleChangeLang(value)
94+
}}
95+
value={currentLang}
96+
>
97+
<SelectTrigger>
98+
<SelectValue placeholder={t('forms:bot_lang.placeholder')} />
99+
</SelectTrigger>
100+
<SelectContent>
101+
{Object.keys(LANGS).map((lang) => {
102+
return (
103+
<SelectItem key={lang} value={lang}>
104+
{LANGS[lang]}
105+
</SelectItem>
106+
)
107+
})}
108+
</SelectContent>
109+
</Select>
110+
</div>
111+
)}
114112
{selectedNode &&
115113
MAP_ACTION[
116114
selectedNode.data.action as unknown as EActionTypes
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import {
2+
Button,
3+
Form,
4+
FormControl,
5+
FormField,
6+
FormItem,
7+
FormLabel,
8+
FormMessage,
9+
Input,
10+
RichEditor,
11+
} from '@/components/ui'
12+
import { TBotSendMail, useBotSendMailSchema } from '@/lib/schema/bot-send-mail'
13+
import { zodResolver } from '@hookform/resolvers/zod'
14+
import { cloneDeep } from 'lodash'
15+
import { useForm } from 'react-hook-form'
16+
import { useTranslation } from 'react-i18next'
17+
import { useFlowCtx } from '..'
18+
19+
export const SendMailContent = () => {
20+
const schema = useBotSendMailSchema()
21+
const form = useForm<TBotSendMail>({
22+
resolver: zodResolver(schema),
23+
mode: 'onChange',
24+
})
25+
const { handleChangeSelectedNode, selectedNode } = useFlowCtx()
26+
27+
const { t } = useTranslation(['forms', 'common'])
28+
29+
const handleSubmit = (data: TBotSendMail) => {
30+
if (!selectedNode) return
31+
32+
const html = document.createElement('div')
33+
html.classList.add('prose')
34+
35+
const tiptap = document.querySelector('.tiptap.ProseMirror')
36+
37+
if (tiptap) {
38+
html.appendChild(tiptap.cloneNode(true))
39+
}
40+
41+
const clonedNode = cloneDeep(selectedNode)
42+
43+
clonedNode.data.sendMail = {
44+
subject: data.subject,
45+
to: data.to,
46+
body: html.outerHTML,
47+
}
48+
49+
handleChangeSelectedNode(clonedNode)
50+
}
51+
52+
console.log(selectedNode)
53+
54+
return (
55+
<Form {...form}>
56+
<form className='space-y-3' onSubmit={form.handleSubmit(handleSubmit)}>
57+
<FormField
58+
control={form.control}
59+
name='subject'
60+
render={({ field }) => {
61+
return (
62+
<FormItem>
63+
<FormLabel required>{t('subject.label')}</FormLabel>
64+
<FormControl>
65+
<Input {...field} placeholder={t('subject.placeholder')} />
66+
</FormControl>
67+
<FormMessage />
68+
</FormItem>
69+
)
70+
}}
71+
/>
72+
<FormField
73+
control={form.control}
74+
name='to'
75+
render={({ field }) => {
76+
return (
77+
<FormItem>
78+
<FormLabel required>{t('mail_to.label')}</FormLabel>
79+
<FormControl>
80+
<Input {...field} placeholder={t('mail_to.placeholder')} />
81+
</FormControl>
82+
<FormMessage />
83+
</FormItem>
84+
)
85+
}}
86+
/>
87+
<FormField
88+
control={form.control}
89+
name='body'
90+
render={({ field }) => {
91+
return (
92+
<FormItem>
93+
<FormLabel required>{t('mail_body.label')}</FormLabel>
94+
<FormControl>
95+
<RichEditor onChange={field.onChange} value={field.value} />
96+
</FormControl>
97+
<FormMessage />
98+
</FormItem>
99+
)
100+
}}
101+
/>
102+
<div className='flex items-center justify-end'>
103+
<Button>{t('common:save')}</Button>
104+
</div>
105+
</form>
106+
</Form>
107+
)
108+
}
109+
110+
export default SendMailContent

0 commit comments

Comments
 (0)