diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index ca734f6ef6..4376bd9c5d 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -205,7 +205,7 @@ export interface IRestApi { removeTestWebhook(workflowId: string): Promise; runWorkflow(runData: IStartRunData): Promise; createNewWorkflow(sendData: IWorkflowDataUpdate): Promise; - updateWorkflow(id: string, data: IWorkflowDataUpdate): Promise; + updateWorkflow(id: string, data: IWorkflowDataUpdate, forceSave?: boolean): Promise; deleteWorkflow(name: string): Promise; getWorkflow(id: string): Promise; getWorkflows(filter?: object): Promise; diff --git a/packages/editor-ui/src/mixins/restApi.ts b/packages/editor-ui/src/mixins/restApi.ts index fecfd5f3a4..111cd81c48 100644 --- a/packages/editor-ui/src/mixins/restApi.ts +++ b/packages/editor-ui/src/mixins/restApi.ts @@ -105,8 +105,8 @@ export const restApi = Vue.extend({ }, // Updates an existing workflow - updateWorkflow: (id: string, data: IWorkflowDataUpdate): Promise => { - return self.restApi().makeRestApiRequest('PATCH', `/workflows/${id}`, data); + updateWorkflow: (id: string, data: IWorkflowDataUpdate, forceSave = false): Promise => { + return self.restApi().makeRestApiRequest('PATCH', `/workflows/${id}${forceSave ? '?forceSave=true' : ''}`, data); }, // Deletes a workflow diff --git a/packages/editor-ui/src/mixins/workflowHelpers.ts b/packages/editor-ui/src/mixins/workflowHelpers.ts index eb424bf84e..41eb90c6e9 100644 --- a/packages/editor-ui/src/mixins/workflowHelpers.ts +++ b/packages/editor-ui/src/mixins/workflowHelpers.ts @@ -703,7 +703,7 @@ export const workflowHelpers = mixins( } }, - async saveCurrentWorkflow({id, name, tags}: {id?: string, name?: string, tags?: string[]} = {}, redirect = true): Promise { + async saveCurrentWorkflow({id, name, tags}: {id?: string, name?: string, tags?: string[]} = {}, redirect = true, forceSave = false): Promise { const currentWorkflow = id || this.$route.params.name; if (!currentWorkflow || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(currentWorkflow)) { @@ -726,7 +726,7 @@ export const workflowHelpers = mixins( workflowDataRequest.hash = this.workflowsStore.workflowHash; - const workflowData = await this.restApi().updateWorkflow(currentWorkflow, workflowDataRequest); + const workflowData = await this.restApi().updateWorkflow(currentWorkflow, workflowDataRequest, forceSave); this.workflowsStore.setWorkflowHash(workflowData.hash); if (name) { @@ -747,6 +747,22 @@ export const workflowHelpers = mixins( } catch (error) { this.uiStore.removeActiveAction('workflowSaving'); + if (error.errorCode === 400 && error.message.startsWith('Your most recent changes may be lost')) { + const overwrite = await this.confirmMessage( + this.$locale.baseText('workflows.concurrentChanges.confirmMessage.message'), + this.$locale.baseText('workflows.concurrentChanges.confirmMessage.title'), + null, + this.$locale.baseText('workflows.concurrentChanges.confirmMessage.confirmButtonText'), + this.$locale.baseText('workflows.concurrentChanges.confirmMessage.cancelButtonText'), + ); + + if (overwrite) { + return this.saveCurrentWorkflow({id, name, tags}, redirect, true); + } + + return false; + } + this.$showMessage({ title: this.$locale.baseText('workflowHelpers.showMessage.title'), message: error.message, diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 29fc447e38..6b997a6253 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1399,6 +1399,10 @@ "workflows.shareModal.notAvailable": "Sharing workflows with others is currently available only on n8n cloud, our hosted offering.", "workflows.shareModal.notAvailable.button": "Explore n8n cloud", "workflows.roles.editor": "Editor", + "workflows.concurrentChanges.confirmMessage.title": "Workflow was edited by someone else", + "workflows.concurrentChanges.confirmMessage.message": "Another user made an edit to this workflow since you last saved it. Do you want to overwrite their changes?", + "workflows.concurrentChanges.confirmMessage.cancelButtonText": "Cancel", + "workflows.concurrentChanges.confirmMessage.confirmButtonText": "Overwrite then save", "importCurlModal.title": "Import cURL command", "importCurlModal.input.label": "cURL Command", "importCurlModal.input.placeholder": "Paste the cURL command here",