Skip to content

Commit 8e67a89

Browse files
committed
fix(ui): prevent duplicate chatflow saves
1 parent 42d593f commit 8e67a89

4 files changed

Lines changed: 46 additions & 11 deletions

File tree

packages/ui/src/ui-component/dialog/SaveChatflowDialog.jsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import PropTypes from 'prop-types'
55
import { Button, Dialog, DialogActions, DialogContent, OutlinedInput, DialogTitle } from '@mui/material'
66
import { StyledButton } from '@/ui-component/button/StyledButton'
77

8-
const SaveChatflowDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
8+
const SaveChatflowDialog = ({ show, dialogProps, onCancel, onConfirm, isSubmitting }) => {
99
const portalElement = document.getElementById('portal')
1010

1111
const [chatflowName, setChatflowName] = useState('')
@@ -21,9 +21,10 @@ const SaveChatflowDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
2121
open={show}
2222
fullWidth
2323
maxWidth='xs'
24-
onClose={onCancel}
24+
onClose={isSubmitting ? undefined : onCancel}
2525
aria-labelledby='alert-dialog-title'
2626
aria-describedby='alert-dialog-description'
27+
disableEscapeKeyDown={isSubmitting}
2728
disableRestoreFocus // needed due to StrictMode
2829
>
2930
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
@@ -41,14 +42,16 @@ const SaveChatflowDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
4142
value={chatflowName}
4243
onChange={(e) => setChatflowName(e.target.value)}
4344
onKeyDown={(e) => {
44-
if (isReadyToSave && e.key === 'Enter') onConfirm(e.target.value)
45+
if (!isSubmitting && isReadyToSave && e.key === 'Enter') onConfirm(e.target.value)
4546
}}
4647
/>
4748
</DialogContent>
4849
<DialogActions>
49-
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
50-
<StyledButton disabled={!isReadyToSave} variant='contained' onClick={() => onConfirm(chatflowName)}>
51-
{dialogProps.confirmButtonName}
50+
<Button disabled={isSubmitting} onClick={onCancel}>
51+
{dialogProps.cancelButtonName}
52+
</Button>
53+
<StyledButton disabled={!isReadyToSave || isSubmitting} variant='contained' onClick={() => onConfirm(chatflowName)}>
54+
{isSubmitting ? 'Saving...' : dialogProps.confirmButtonName}
5255
</StyledButton>
5356
</DialogActions>
5457
</Dialog>
@@ -61,7 +64,8 @@ SaveChatflowDialog.propTypes = {
6164
show: PropTypes.bool,
6265
dialogProps: PropTypes.object,
6366
onCancel: PropTypes.func,
64-
onConfirm: PropTypes.func
67+
onConfirm: PropTypes.func,
68+
isSubmitting: PropTypes.bool
6569
}
6670

6771
export default SaveChatflowDialog

packages/ui/src/views/agentflowsv2/Canvas.jsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const AgentflowCanvas = () => {
125125
const createNewChatflowApi = useApi(chatflowsApi.createNewChatflow)
126126
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
127127
const getSpecificChatflowApi = useApi(chatflowsApi.getSpecificChatflow)
128+
const saveInProgressRef = useRef(false)
128129

129130
// ==============================|| Events & Actions ||============================== //
130131

@@ -217,6 +218,9 @@ const AgentflowCanvas = () => {
217218
}
218219

219220
const handleSaveFlow = (chatflowName) => {
221+
if (saveInProgressRef.current || createNewChatflowApi.loading || updateChatflowApi.loading) return
222+
saveInProgressRef.current = true
223+
220224
if (reactFlowInstance) {
221225
const nodes = reactFlowInstance.getNodes().map((node) => {
222226
const nodeData = cloneDeep(node.data)
@@ -252,6 +256,8 @@ const AgentflowCanvas = () => {
252256
}
253257
updateChatflowApi.request(chatflow.id, updateBody)
254258
}
259+
} else {
260+
saveInProgressRef.current = false
255261
}
256262
}
257263

@@ -555,11 +561,13 @@ const AgentflowCanvas = () => {
555561
// Create new chatflow successful
556562
useEffect(() => {
557563
if (createNewChatflowApi.data) {
564+
saveInProgressRef.current = false
558565
const chatflow = createNewChatflowApi.data
559566
dispatch({ type: SET_CHATFLOW, chatflow })
560567
saveChatflowSuccess()
561568
window.history.replaceState(state, null, `/v2/agentcanvas/${chatflow.id}`)
562569
} else if (createNewChatflowApi.error) {
570+
saveInProgressRef.current = false
563571
errorFailed(`Failed to save ${canvasTitle}: ${createNewChatflowApi.error.response.data.message}`)
564572
}
565573

@@ -569,9 +577,11 @@ const AgentflowCanvas = () => {
569577
// Update chatflow successful
570578
useEffect(() => {
571579
if (updateChatflowApi.data) {
580+
saveInProgressRef.current = false
572581
dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data })
573582
saveChatflowSuccess()
574583
} else if (updateChatflowApi.error) {
584+
saveInProgressRef.current = false
575585
errorFailed(`Failed to save ${canvasTitle}: ${updateChatflowApi.error.response.data.message}`)
576586
}
577587

@@ -711,6 +721,7 @@ const AgentflowCanvas = () => {
711721
handleLoadFlow={handleLoadFlow}
712722
isAgentCanvas={true}
713723
isAgentflowV2={true}
724+
isSaveLoading={createNewChatflowApi.loading || updateChatflowApi.loading}
714725
/>
715726
</Toolbar>
716727
</AppBar>

packages/ui/src/views/canvas/CanvasHeader.jsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ const LockedScheduleSwitch = styled(ScheduleSwitch, { shouldForwardProp: (prop)
120120

121121
// ==============================|| CANVAS HEADER ||============================== //
122122

123-
const CanvasHeader = ({ chatflow, isAgentCanvas, isAgentflowV2, handleSaveFlow, handleDeleteFlow, handleLoadFlow }) => {
123+
const CanvasHeader = ({ chatflow, isAgentCanvas, isAgentflowV2, isSaveLoading, handleSaveFlow, handleDeleteFlow, handleLoadFlow }) => {
124124
const theme = useTheme()
125125
const dispatch = useDispatch()
126126
const navigate = useNavigate()
@@ -319,11 +319,13 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, isAgentflowV2, handleSaveFlow,
319319
}
320320

321321
const onSaveChatflowClick = () => {
322+
if (isSaveLoading) return
322323
if (chatflow.id) handleSaveFlow(flowName)
323324
else setFlowDialogOpen(true)
324325
}
325326

326327
const onConfirmSaveName = (flowName) => {
328+
if (isSaveLoading) return
327329
setFlowDialogOpen(false)
328330
setSavePermission(isAgentCanvas ? 'agentflows:update' : 'chatflows:update')
329331
handleSaveFlow(flowName)
@@ -593,7 +595,7 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, isAgentflowV2, handleSaveFlow,
593595
</ButtonBase>
594596
)}
595597
<Available permission={savePermission}>
596-
<ButtonBase title={`Save ${title}`} sx={{ borderRadius: '50%', mr: 2 }}>
598+
<ButtonBase disabled={isSaveLoading} title={`Save ${title}`} sx={{ borderRadius: '50%', mr: 2 }}>
597599
<Avatar
598600
variant='rounded'
599601
sx={{
@@ -605,7 +607,8 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, isAgentflowV2, handleSaveFlow,
605607
'&:hover': {
606608
background: theme.palette.canvasHeader.saveDark,
607609
color: theme.palette.canvasHeader.saveLight
608-
}
610+
},
611+
opacity: isSaveLoading ? 0.6 : 1
609612
}}
610613
color='inherit'
611614
onClick={onSaveChatflowClick}
@@ -653,6 +656,7 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, isAgentflowV2, handleSaveFlow,
653656
}}
654657
onCancel={() => setFlowDialogOpen(false)}
655658
onConfirm={onConfirmSaveName}
659+
isSubmitting={isSaveLoading}
656660
/>
657661
{apiDialogOpen && <APICodeDialog show={apiDialogOpen} dialogProps={apiDialogProps} onCancel={() => setAPIDialogOpen(false)} />}
658662
<ViewMessagesDialog
@@ -690,7 +694,8 @@ CanvasHeader.propTypes = {
690694
handleDeleteFlow: PropTypes.func,
691695
handleLoadFlow: PropTypes.func,
692696
isAgentCanvas: PropTypes.bool,
693-
isAgentflowV2: PropTypes.bool
697+
isAgentflowV2: PropTypes.bool,
698+
isSaveLoading: PropTypes.bool
694699
}
695700

696701
export default CanvasHeader

packages/ui/src/views/canvas/index.jsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ const Canvas = () => {
113113
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
114114
const getSpecificChatflowApi = useApi(chatflowsApi.getSpecificChatflow)
115115
const getHasChatflowChangedApi = useApi(chatflowsApi.getHasChatflowChanged)
116+
const saveInProgressRef = useRef(false)
116117

117118
// ==============================|| Events & Actions ||============================== //
118119

@@ -209,6 +210,10 @@ const Canvas = () => {
209210
}
210211

211212
const handleSaveFlow = async (chatflowName) => {
213+
if (saveInProgressRef.current || createNewChatflowApi.loading || updateChatflowApi.loading || getHasChatflowChangedApi.loading)
214+
return
215+
saveInProgressRef.current = true
216+
212217
if (reactFlowInstance) {
213218
const nodes = reactFlowInstance.getNodes().map((node) => {
214219
const nodeData = cloneDeep(node.data)
@@ -241,6 +246,8 @@ const Canvas = () => {
241246
setFlowData(flowData)
242247
getHasChatflowChangedApi.request(chatflow.id, lastUpdatedDateTime)
243248
}
249+
} else {
250+
saveInProgressRef.current = false
244251
}
245252
}
246253

@@ -429,11 +436,13 @@ const Canvas = () => {
429436
// Create new chatflow successful
430437
useEffect(() => {
431438
if (createNewChatflowApi.data) {
439+
saveInProgressRef.current = false
432440
const chatflow = createNewChatflowApi.data
433441
dispatch({ type: SET_CHATFLOW, chatflow })
434442
saveChatflowSuccess()
435443
window.history.replaceState(state, null, `/${isAgentCanvas ? 'agentcanvas' : 'canvas'}/${chatflow.id}`)
436444
} else if (createNewChatflowApi.error) {
445+
saveInProgressRef.current = false
437446
errorFailed(`Failed to retrieve ${canvasTitle}: ${createNewChatflowApi.error.response.data.message}`)
438447
}
439448

@@ -443,10 +452,12 @@ const Canvas = () => {
443452
// Update chatflow successful
444453
useEffect(() => {
445454
if (updateChatflowApi.data) {
455+
saveInProgressRef.current = false
446456
dispatch({ type: SET_CHATFLOW, chatflow: updateChatflowApi.data })
447457
setLasUpdatedDateTime(updateChatflowApi.data.updatedDate)
448458
saveChatflowSuccess()
449459
} else if (updateChatflowApi.error) {
460+
saveInProgressRef.current = false
450461
errorFailed(`Failed to retrieve ${canvasTitle}: ${updateChatflowApi.error.response.data.message}`)
451462
}
452463

@@ -466,6 +477,7 @@ const Canvas = () => {
466477
const isConfirmed = await confirm(confirmPayload)
467478

468479
if (!isConfirmed) {
480+
saveInProgressRef.current = false
469481
return
470482
}
471483
}
@@ -478,6 +490,8 @@ const Canvas = () => {
478490

479491
if (getHasChatflowChangedApi.data) {
480492
checkIfHasChanged()
493+
} else if (getHasChatflowChangedApi.error) {
494+
saveInProgressRef.current = false
481495
}
482496

483497
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -577,6 +591,7 @@ const Canvas = () => {
577591
handleDeleteFlow={handleDeleteFlow}
578592
handleLoadFlow={handleLoadFlow}
579593
isAgentCanvas={isAgentCanvas}
594+
isSaveLoading={createNewChatflowApi.loading || updateChatflowApi.loading || getHasChatflowChangedApi.loading}
580595
/>
581596
</Toolbar>
582597
</AppBar>

0 commit comments

Comments
 (0)