Skip to content

Commit 29a53b3

Browse files
committed
feat: add condition for prompt and variables
1 parent f2a74b1 commit 29a53b3

File tree

13 files changed

+555
-135
lines changed

13 files changed

+555
-135
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { CONDITIONAL_OPERATOR } from '@/constants'
2+
import {
3+
TCompareValue,
4+
useCompareValueSchema,
5+
} from '@/lib/schema/compare-value'
6+
import { zodResolver } from '@hookform/resolvers/zod'
7+
import { useForm } from 'react-hook-form'
8+
import { useTranslation } from 'react-i18next'
9+
import {
10+
Form,
11+
FormControl,
12+
FormField,
13+
FormItem,
14+
FormLabel,
15+
FormMessage,
16+
Input,
17+
Select,
18+
SelectContent,
19+
SelectItem,
20+
SelectTrigger,
21+
SelectValue,
22+
} from '../ui'
23+
24+
type Props = {
25+
onSubmit?: (data: TCompareValue) => void
26+
defaultValues?: TCompareValue
27+
id?: string
28+
}
29+
30+
const ConditionForm = ({
31+
defaultValues,
32+
onSubmit,
33+
id = 'condition-form',
34+
}: Props) => {
35+
const schema = useCompareValueSchema()
36+
const form = useForm<TCompareValue>({
37+
defaultValues,
38+
resolver: zodResolver(schema),
39+
})
40+
const { t } = useTranslation(['forms', 'flowDetail'])
41+
42+
const handleSubmit = form.handleSubmit((data) => {
43+
onSubmit?.(data)
44+
})
45+
46+
return (
47+
<Form {...form}>
48+
<form className='space-y-3' onSubmit={handleSubmit} id={id}>
49+
<div className='flex space-x-3'>
50+
<FormField
51+
name='operator'
52+
control={form.control}
53+
render={({ field }) => (
54+
<FormItem className='w-full'>
55+
<FormLabel>{t('operator.label')}</FormLabel>
56+
<Select
57+
onValueChange={field.onChange}
58+
defaultValue={field.value}
59+
>
60+
<FormControl>
61+
<SelectTrigger>
62+
<SelectValue placeholder={t('operator.placeholder')} />
63+
</SelectTrigger>
64+
</FormControl>
65+
<SelectContent>
66+
{CONDITIONAL_OPERATOR.map((c) => {
67+
return (
68+
<SelectItem key={c} value={c}>
69+
{
70+
// @ts-ignore
71+
t(`flowDetail:compare_types.${c}`)
72+
}
73+
</SelectItem>
74+
)
75+
})}
76+
</SelectContent>
77+
</Select>
78+
<FormMessage />
79+
</FormItem>
80+
)}
81+
/>
82+
<FormField
83+
name='value'
84+
control={form.control}
85+
render={({ field }) => {
86+
return (
87+
<FormItem className='w-full'>
88+
<FormLabel>{t('value.label')}</FormLabel>
89+
<FormControl>
90+
<Input {...field} placeholder={t('value.placeholder')} />
91+
</FormControl>
92+
<FormMessage />
93+
</FormItem>
94+
)
95+
}}
96+
/>
97+
</div>
98+
</form>
99+
</Form>
100+
)
101+
}
102+
103+
export default ConditionForm
Lines changed: 146 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,150 @@
1-
type Props = {}
1+
import ConditionForm from '@/components/forms/condition'
2+
import {
3+
Button,
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
DialogTrigger,
11+
} from '@/components/ui'
12+
import { TCompareValue } from '@/lib/schema/compare-value'
13+
import { useTranslation } from 'react-i18next'
14+
import { useFlowCtx } from '.'
215

3-
const ConditionDialog = (_props: Props) => {
4-
return <div>ConditionDialog</div>
16+
export const ConditionDialog = () => {
17+
const { t } = useTranslation(['flowDetail', 'common'])
18+
const {
19+
selectedEdge,
20+
getNode,
21+
handleChangeSelectedEdge,
22+
setNodes,
23+
handleDeleteEdgeById,
24+
} = useFlowCtx()
25+
26+
const getOperatorValue = () => {
27+
if (!selectedEdge)
28+
return {
29+
operator: undefined,
30+
value: '',
31+
}
32+
33+
const node = getNode(selectedEdge.source)
34+
35+
if (!node?.data?.nextActions?.length)
36+
return {
37+
operator: undefined,
38+
value: '',
39+
}
40+
41+
const nextAction = node.data.nextActions.find(
42+
(action: any) => action.id === selectedEdge.target,
43+
)
44+
45+
if (!nextAction)
46+
return {
47+
operator: undefined,
48+
value: '',
49+
}
50+
51+
return {
52+
operator: nextAction?.condition,
53+
value: nextAction?.value || '',
54+
}
55+
}
56+
57+
const handleSave = (data: TCompareValue) => {
58+
if (!selectedEdge) return
59+
60+
const node = getNode(selectedEdge.source)
61+
62+
if (!node?.data?.nextActions?.length) return
63+
64+
const nextAction = node.data.nextActions.find(
65+
(action: any) => action.id === selectedEdge.target,
66+
)
67+
68+
if (!nextAction) return
69+
70+
node.data.nextActions = node.data.nextActions.map((action: any) => {
71+
if (action.id === selectedEdge.target) {
72+
return {
73+
...action,
74+
condition: data.operator,
75+
value: data.value,
76+
}
77+
}
78+
79+
return action
80+
})
81+
82+
setNodes((prev) => {
83+
return prev.map((n) => {
84+
if (n.id === node.id) {
85+
return node
86+
}
87+
88+
return n
89+
})
90+
})
91+
92+
handleChangeSelectedEdge(null)
93+
}
94+
95+
const handleCancel = () => {
96+
if (!selectedEdge) return
97+
98+
const node = getNode(selectedEdge.source)
99+
100+
if (!node) return
101+
102+
if (!node.data?.nextActions?.length) return
103+
104+
const nextAction = node.data?.nextActions.find(
105+
(action: any) => action.id === selectedEdge.target,
106+
)
107+
108+
if (!nextAction) return
109+
110+
if (!nextAction.condition) {
111+
handleDeleteEdgeById(selectedEdge.id)
112+
}
113+
114+
handleChangeSelectedEdge(null)
115+
}
116+
117+
return (
118+
<Dialog
119+
open={Boolean(selectedEdge)}
120+
onOpenChange={(open) => {
121+
if (!open) {
122+
handleCancel()
123+
}
124+
}}
125+
>
126+
<DialogTrigger asChild>
127+
<div className='hidden' />
128+
</DialogTrigger>
129+
<DialogContent>
130+
<DialogHeader>
131+
<DialogTitle>{t('condition_dialog.title')}</DialogTitle>
132+
<DialogDescription>{t('condition_dialog.desc')}</DialogDescription>
133+
</DialogHeader>
134+
<ConditionForm
135+
defaultValues={{
136+
...getOperatorValue(),
137+
}}
138+
onSubmit={handleSave}
139+
/>
140+
<DialogFooter>
141+
<Button type='submit' form='condition-form'>
142+
{t('common:save')}
143+
</Button>
144+
</DialogFooter>
145+
</DialogContent>
146+
</Dialog>
147+
)
5148
}
6149

7150
export default ConditionDialog

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,16 +139,16 @@ export const MAP_MESSAGE_TYPE: Record<
139139
}
140140

141141
export const CONDITIONAL_OPERATOR = [
142-
'==',
143-
'!=',
144-
'>',
145-
'>=',
146-
'<',
147-
'<=',
142+
'equal',
143+
'not_equal',
144+
'greater_than',
145+
'greater_than_or_equal',
146+
'less_than',
147+
'less_than_or_equal',
148148
'contains',
149149
'not_contains',
150-
'in',
151-
'not_in',
150+
'match',
151+
'not_match',
152152
]
153153

154154
export const MAP_GRAMMAR_TYPE: Record<EGrammarTypes, string> = {

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

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Button } from '@/components/ui'
2-
import _ from 'lodash'
32
import { X } from 'lucide-react'
4-
import { BaseEdge, EdgeProps, getSmoothStepPath, useReactFlow } from 'reactflow'
3+
import { BaseEdge, EdgeProps, getSmoothStepPath } from 'reactflow'
54
import { useUnmount } from 'usehooks-ts'
5+
import { useFlowCtx } from '.'
66

77
const foreignObjectSize = 16
88

@@ -20,12 +20,9 @@ export const Edge = ({
2020
data = {
2121
deletable: true,
2222
},
23-
source,
24-
target,
2523
}: EdgeProps<{
2624
deletable?: boolean
2725
}>) => {
28-
const { setEdges, getNode, setNodes } = useReactFlow()
2926
const [edgePath, labelX, labelY] = getSmoothStepPath({
3027
sourceX,
3128
sourceY,
@@ -34,52 +31,10 @@ export const Edge = ({
3431
targetY,
3532
targetPosition,
3633
})
37-
38-
/**
39-
* Handles the delete action for an edge.
40-
* If the edge is not deletable or the source node does not exist, the function returns early.
41-
* If the source node exists, it creates a deep clone of the node and modifies its data accordingly.
42-
* Finally, it updates the nodes and edges state to reflect the changes.
43-
*/
44-
const handleDelete = () => {
45-
if (!data?.deletable) return
46-
47-
const sourceNode = getNode(source)
48-
49-
if (sourceNode) {
50-
const cloned = _.cloneDeep(sourceNode)
51-
52-
if (cloned.data?.nextAction) {
53-
delete cloned.data.nextAction
54-
}
55-
56-
if (cloned.data?.nextActions) {
57-
cloned.data.nextActions = cloned.data.nextActions.filter(
58-
(nextAction: any) => nextAction.id !== target,
59-
)
60-
61-
if (cloned.data.nextActions.length === 0) {
62-
delete cloned.data.nextActions
63-
}
64-
}
65-
66-
setNodes((nodes) =>
67-
nodes.map((node) => {
68-
if (node.id === source) {
69-
return cloned
70-
}
71-
72-
return node
73-
}),
74-
)
75-
}
76-
77-
setEdges((edges) => edges.filter((edge) => edge.id !== id))
78-
}
34+
const { handleDeleteEdgeById } = useFlowCtx()
7935

8036
useUnmount(() => {
81-
console.log('edge unmount')
82-
handleDelete()
37+
handleDeleteEdgeById(id)
8338
})
8439

8540
return (
@@ -105,7 +60,7 @@ export const Edge = ({
10560
size='icon'
10661
className='w-4 h-4 opacity-0 scale-50 invisible btn'
10762
variant='destructive'
108-
onClick={handleDelete}
63+
onClick={() => handleDeleteEdgeById(id)}
10964
>
11065
<X className='w-2 h-2' />
11166
</Button>

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { useTranslation } from 'react-i18next'
1313
import { useBlocker } from 'react-router-dom'
1414
import ReactFlow, { Background, BackgroundVariant } from 'reactflow'
1515
import { Actions } from './actions'
16+
import ConditionDialog from './condition-dialog'
1617
import { Controls } from './controls'
1718
import { edgeTypes } from './edge-types'
1819
import { useFlowCtx } from './flow-provider'
@@ -99,6 +100,7 @@ export const FlowInside = () => {
99100
<Toolbar />
100101
<Actions />
101102
<NodeDialog />
103+
<ConditionDialog />
102104
</div>
103105
)
104106
}

0 commit comments

Comments
 (0)