mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Properly update workflow info in main header (#9789)
This commit is contained in:
@@ -2,6 +2,7 @@ import type { RouteHandler } from 'cypress/types/net-stubbing';
|
||||
import { WorkflowPage } from '../pages';
|
||||
import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab';
|
||||
import executionOutOfMemoryServerResponse from '../fixtures/responses/execution-out-of-memory-server-response.json';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const executionsTab = new WorkflowExecutionsTab();
|
||||
@@ -136,15 +137,90 @@ describe('Current Workflow Executions', () => {
|
||||
executionsTab.getters.executionListItems().eq(14).should('not.be.visible');
|
||||
});
|
||||
|
||||
it('should show workflow data in executions tab after hard reload', () => {
|
||||
it('should show workflow data in executions tab after hard reload and modify name and tags', () => {
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
|
||||
workflowPage.getters.workflowTags().click();
|
||||
getVisibleSelect().find('li:contains("Manage tags")').click();
|
||||
cy.get('button:contains("Add new")').click();
|
||||
cy.getByTestId('tags-table').find('input').type('nutag').type('{enter}');
|
||||
cy.get('button:contains("Done")').click();
|
||||
|
||||
cy.reload();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.workflowTags().click();
|
||||
workflowPage.getters.tagsInDropdown().first().should('have.text', 'nutag').click();
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
|
||||
let newWorkflowName = 'Renamed workflow';
|
||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 3);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
newWorkflowName = 'New workflow';
|
||||
workflowPage.actions.renameWorkflow(newWorkflowName);
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
workflowPage.getters.workflowTags().click();
|
||||
workflowPage.getters.tagsDropdown().find('.el-tag__close').first().click();
|
||||
cy.get('body').click(0, 0);
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
checkMainHeaderELements();
|
||||
workflowPage.getters.saveButton().find('button').should('not.exist');
|
||||
workflowPage.getters.tagPills().should('have.length', 2);
|
||||
workflowPage.getters
|
||||
.workflowNameInputContainer()
|
||||
.invoke('attr', 'title')
|
||||
.should('eq', newWorkflowName);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onBeforeMount, onMounted } from 'vue';
|
||||
import type { RouteLocation, RouteLocationRaw } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import WorkflowDetails from '@/components/MainHeader/WorkflowDetails.vue';
|
||||
import TabBar from '@/components/MainHeader/TabBar.vue';
|
||||
import {
|
||||
MAIN_HEADER_TABS,
|
||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||
STICKY_NODE_TYPE,
|
||||
VIEWS,
|
||||
} from '@/constants';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useExecutionsStore } from '@/stores/executions.store';
|
||||
import { usePushConnection } from '@/composables/usePushConnection';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const locale = useI18n();
|
||||
const pushConnection = usePushConnection({ router });
|
||||
const ndvStore = useNDVStore();
|
||||
const uiStore = useUIStore();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const executionsStore = useExecutionsStore();
|
||||
|
||||
const activeHeaderTab = ref(MAIN_HEADER_TABS.WORKFLOW);
|
||||
const workflowToReturnTo = ref('');
|
||||
const executionToReturnTo = ref('');
|
||||
const dirtyState = ref(false);
|
||||
|
||||
const tabBarItems = computed(() => [
|
||||
{ value: MAIN_HEADER_TABS.WORKFLOW, label: locale.baseText('generic.editor') },
|
||||
{ value: MAIN_HEADER_TABS.EXECUTIONS, label: locale.baseText('generic.executions') },
|
||||
]);
|
||||
|
||||
const activeNode = computed(() => ndvStore.activeNode);
|
||||
const hideMenuBar = computed(() =>
|
||||
Boolean(activeNode.value && activeNode.value.type !== STICKY_NODE_TYPE),
|
||||
);
|
||||
const workflow = computed(() => workflowsStore.workflow);
|
||||
const workflowId = computed(() =>
|
||||
String(router.currentRoute.value.params.name || workflowsStore.workflowId),
|
||||
);
|
||||
const onWorkflowPage = computed(() => !!(route.meta.nodeView || route.meta.keepWorkflowAlive));
|
||||
const readOnly = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||
|
||||
watch(route, (to, from) => {
|
||||
syncTabsWithRoute(to, from);
|
||||
});
|
||||
|
||||
onBeforeMount(() => {
|
||||
pushConnection.initialize();
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
dirtyState.value = uiStore.stateIsDirty;
|
||||
syncTabsWithRoute(route);
|
||||
});
|
||||
|
||||
function syncTabsWithRoute(to: RouteLocation, from?: RouteLocation): void {
|
||||
if (
|
||||
to.name === VIEWS.EXECUTION_HOME ||
|
||||
to.name === VIEWS.WORKFLOW_EXECUTIONS ||
|
||||
to.name === VIEWS.EXECUTION_PREVIEW
|
||||
) {
|
||||
activeHeaderTab.value = MAIN_HEADER_TABS.EXECUTIONS;
|
||||
} else if (
|
||||
to.name === VIEWS.WORKFLOW ||
|
||||
to.name === VIEWS.NEW_WORKFLOW ||
|
||||
to.name === VIEWS.EXECUTION_DEBUG
|
||||
) {
|
||||
activeHeaderTab.value = MAIN_HEADER_TABS.WORKFLOW;
|
||||
}
|
||||
|
||||
if (to.params.name !== 'new' && typeof to.params.name === 'string') {
|
||||
workflowToReturnTo.value = to.params.name;
|
||||
}
|
||||
|
||||
if (
|
||||
from?.name === VIEWS.EXECUTION_PREVIEW &&
|
||||
to.params.name === from.params.name &&
|
||||
typeof from.params.executionId === 'string'
|
||||
) {
|
||||
executionToReturnTo.value = from.params.executionId;
|
||||
}
|
||||
}
|
||||
|
||||
function onTabSelected(tab: MAIN_HEADER_TABS, event: MouseEvent) {
|
||||
const openInNewTab = event.ctrlKey || event.metaKey;
|
||||
|
||||
switch (tab) {
|
||||
case MAIN_HEADER_TABS.WORKFLOW:
|
||||
void navigateToWorkflowView(openInNewTab);
|
||||
break;
|
||||
|
||||
case MAIN_HEADER_TABS.EXECUTIONS:
|
||||
void navigateToExecutionsView(openInNewTab);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function navigateToWorkflowView(openInNewTab: boolean) {
|
||||
let routeToNavigateTo: RouteLocationRaw;
|
||||
if (!['', 'new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(workflowToReturnTo.value)) {
|
||||
routeToNavigateTo = {
|
||||
name: VIEWS.WORKFLOW,
|
||||
params: { name: workflowToReturnTo.value },
|
||||
};
|
||||
} else {
|
||||
routeToNavigateTo = { name: VIEWS.NEW_WORKFLOW };
|
||||
}
|
||||
|
||||
if (openInNewTab) {
|
||||
const { href } = router.resolve(routeToNavigateTo);
|
||||
window.open(href, '_blank');
|
||||
} else if (route.name !== routeToNavigateTo.name) {
|
||||
if (route.name === VIEWS.NEW_WORKFLOW) {
|
||||
uiStore.stateIsDirty = dirtyState.value;
|
||||
}
|
||||
activeHeaderTab.value = MAIN_HEADER_TABS.WORKFLOW;
|
||||
await router.push(routeToNavigateTo);
|
||||
}
|
||||
}
|
||||
|
||||
async function navigateToExecutionsView(openInNewTab: boolean) {
|
||||
const routeWorkflowId =
|
||||
workflowId.value === PLACEHOLDER_EMPTY_WORKFLOW_ID ? 'new' : workflowId.value;
|
||||
const executionToReturnToValue = executionsStore.activeExecution?.id || executionToReturnTo.value;
|
||||
const routeToNavigateTo: RouteLocationRaw = executionToReturnToValue
|
||||
? {
|
||||
name: VIEWS.EXECUTION_PREVIEW,
|
||||
params: { name: routeWorkflowId, executionId: executionToReturnToValue },
|
||||
}
|
||||
: {
|
||||
name: VIEWS.EXECUTION_HOME,
|
||||
params: { name: routeWorkflowId },
|
||||
};
|
||||
|
||||
if (openInNewTab) {
|
||||
const { href } = router.resolve(routeToNavigateTo);
|
||||
window.open(href, '_blank');
|
||||
} else if (route.name !== routeToNavigateTo.name) {
|
||||
dirtyState.value = uiStore.stateIsDirty;
|
||||
workflowToReturnTo.value = workflowId.value;
|
||||
activeHeaderTab.value = MAIN_HEADER_TABS.EXECUTIONS;
|
||||
await router.push(routeToNavigateTo);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div :class="{ 'main-header': true, expanded: !uiStore.sidebarMenuCollapsed }">
|
||||
@@ -14,194 +172,6 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { RouteLocation, RouteLocationRaw } from 'vue-router';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { mapStores } from 'pinia';
|
||||
import WorkflowDetails from '@/components/MainHeader/WorkflowDetails.vue';
|
||||
import TabBar from '@/components/MainHeader/TabBar.vue';
|
||||
import {
|
||||
MAIN_HEADER_TABS,
|
||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||
STICKY_NODE_TYPE,
|
||||
VIEWS,
|
||||
} from '@/constants';
|
||||
import type { INodeUi, ITabBarItem, IWorkflowDb } from '@/Interface';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useExecutionsStore } from '@/stores/executions.store';
|
||||
import { usePushConnection } from '@/composables/usePushConnection';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MainHeader',
|
||||
components: {
|
||||
WorkflowDetails,
|
||||
TabBar,
|
||||
},
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const pushConnection = usePushConnection({ router });
|
||||
|
||||
return {
|
||||
pushConnection,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeHeaderTab: MAIN_HEADER_TABS.WORKFLOW,
|
||||
workflowToReturnTo: '',
|
||||
executionToReturnTo: '',
|
||||
dirtyState: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(
|
||||
useNDVStore,
|
||||
useUIStore,
|
||||
useSourceControlStore,
|
||||
useWorkflowsStore,
|
||||
useExecutionsStore,
|
||||
),
|
||||
tabBarItems(): ITabBarItem[] {
|
||||
return [
|
||||
{ value: MAIN_HEADER_TABS.WORKFLOW, label: this.$locale.baseText('generic.editor') },
|
||||
{ value: MAIN_HEADER_TABS.EXECUTIONS, label: this.$locale.baseText('generic.executions') },
|
||||
];
|
||||
},
|
||||
activeNode(): INodeUi | null {
|
||||
return this.ndvStore.activeNode;
|
||||
},
|
||||
hideMenuBar(): boolean {
|
||||
return Boolean(this.activeNode && this.activeNode.type !== STICKY_NODE_TYPE);
|
||||
},
|
||||
workflow(): IWorkflowDb {
|
||||
return this.workflowsStore.workflow;
|
||||
},
|
||||
currentWorkflow(): string {
|
||||
return String(this.$route.params.name || this.workflowsStore.workflowId);
|
||||
},
|
||||
onWorkflowPage(): boolean {
|
||||
return !!(this.$route.meta.nodeView || this.$route.meta.keepWorkflowAlive);
|
||||
},
|
||||
readOnly(): boolean {
|
||||
return this.sourceControlStore.preferences.branchReadOnly;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route(to, from) {
|
||||
this.syncTabsWithRoute(to, from);
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
this.pushConnection.initialize();
|
||||
},
|
||||
mounted() {
|
||||
this.dirtyState = this.uiStore.stateIsDirty;
|
||||
this.syncTabsWithRoute(this.$route);
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.pushConnection.terminate();
|
||||
},
|
||||
methods: {
|
||||
syncTabsWithRoute(to: RouteLocation, from?: RouteLocation): void {
|
||||
if (
|
||||
to.name === VIEWS.EXECUTION_HOME ||
|
||||
to.name === VIEWS.WORKFLOW_EXECUTIONS ||
|
||||
to.name === VIEWS.EXECUTION_PREVIEW
|
||||
) {
|
||||
this.activeHeaderTab = MAIN_HEADER_TABS.EXECUTIONS;
|
||||
} else if (
|
||||
to.name === VIEWS.WORKFLOW ||
|
||||
to.name === VIEWS.NEW_WORKFLOW ||
|
||||
to.name === VIEWS.EXECUTION_DEBUG
|
||||
) {
|
||||
this.activeHeaderTab = MAIN_HEADER_TABS.WORKFLOW;
|
||||
}
|
||||
|
||||
if (to.params.name !== 'new' && typeof to.params.name === 'string') {
|
||||
this.workflowToReturnTo = to.params.name;
|
||||
}
|
||||
|
||||
if (
|
||||
from?.name === VIEWS.EXECUTION_PREVIEW &&
|
||||
to.params.name === from.params.name &&
|
||||
typeof from.params.executionId === 'string'
|
||||
) {
|
||||
this.executionToReturnTo = from.params.executionId;
|
||||
}
|
||||
},
|
||||
onTabSelected(tab: MAIN_HEADER_TABS, event: MouseEvent) {
|
||||
const openInNewTab = event.ctrlKey || event.metaKey;
|
||||
|
||||
switch (tab) {
|
||||
case MAIN_HEADER_TABS.WORKFLOW:
|
||||
void this.navigateToWorkflowView(openInNewTab);
|
||||
break;
|
||||
|
||||
case MAIN_HEADER_TABS.EXECUTIONS:
|
||||
void this.navigateToExecutionsView(openInNewTab);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
async navigateToWorkflowView(openInNewTab: boolean) {
|
||||
let routeToNavigateTo: RouteLocationRaw;
|
||||
if (!['', 'new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(this.workflowToReturnTo)) {
|
||||
routeToNavigateTo = {
|
||||
name: VIEWS.WORKFLOW,
|
||||
params: { name: this.workflowToReturnTo },
|
||||
};
|
||||
} else {
|
||||
routeToNavigateTo = { name: VIEWS.NEW_WORKFLOW };
|
||||
}
|
||||
|
||||
if (openInNewTab) {
|
||||
const { href } = this.$router.resolve(routeToNavigateTo);
|
||||
window.open(href, '_blank');
|
||||
} else if (this.$route.name !== routeToNavigateTo.name) {
|
||||
if (this.$route.name === VIEWS.NEW_WORKFLOW) {
|
||||
this.uiStore.stateIsDirty = this.dirtyState;
|
||||
}
|
||||
this.activeHeaderTab = MAIN_HEADER_TABS.WORKFLOW;
|
||||
await this.$router.push(routeToNavigateTo);
|
||||
}
|
||||
},
|
||||
|
||||
async navigateToExecutionsView(openInNewTab: boolean) {
|
||||
const routeWorkflowId =
|
||||
this.currentWorkflow === PLACEHOLDER_EMPTY_WORKFLOW_ID ? 'new' : this.currentWorkflow;
|
||||
const executionToReturnTo =
|
||||
this.executionsStore.activeExecution?.id || this.executionToReturnTo;
|
||||
const routeToNavigateTo: RouteLocationRaw = executionToReturnTo
|
||||
? {
|
||||
name: VIEWS.EXECUTION_PREVIEW,
|
||||
params: { name: routeWorkflowId, executionId: executionToReturnTo },
|
||||
}
|
||||
: {
|
||||
name: VIEWS.EXECUTION_HOME,
|
||||
params: { name: routeWorkflowId },
|
||||
};
|
||||
|
||||
if (openInNewTab) {
|
||||
const { href } = this.$router.resolve(routeToNavigateTo);
|
||||
window.open(href, '_blank');
|
||||
} else if (this.$route.name !== routeToNavigateTo.name) {
|
||||
this.dirtyState = this.uiStore.stateIsDirty;
|
||||
this.workflowToReturnTo = this.currentWorkflow;
|
||||
this.activeHeaderTab = MAIN_HEADER_TABS.EXECUTIONS;
|
||||
await this.$router.push(routeToNavigateTo);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.main-header {
|
||||
background-color: var(--color-background-xlight);
|
||||
|
||||
@@ -110,7 +110,7 @@ export default defineComponent({
|
||||
default: () => ({}),
|
||||
},
|
||||
execution: {
|
||||
type: Object as PropType<ExecutionSummary>,
|
||||
type: Object as PropType<ExecutionSummary> | null,
|
||||
default: null,
|
||||
},
|
||||
loadingMore: {
|
||||
@@ -179,10 +179,10 @@ export default defineComponent({
|
||||
},
|
||||
methods: {
|
||||
async onDeleteCurrentExecution(): Promise<void> {
|
||||
this.$emit('execution:delete', this.execution.id);
|
||||
this.$emit('execution:delete', this.execution?.id);
|
||||
},
|
||||
async onStopExecution(): Promise<void> {
|
||||
this.$emit('execution:stop', this.execution.id);
|
||||
this.$emit('execution:stop', this.execution?.id);
|
||||
},
|
||||
async onRetryExecution(payload: { execution: ExecutionSummary; command: string }) {
|
||||
const loadWorkflow = payload.command === 'current-workflow';
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { ElDropdown } from 'element-plus';
|
||||
import { useExecutionDebugging } from '@/composables/useExecutionDebugging';
|
||||
@@ -165,7 +166,7 @@ export default defineComponent({
|
||||
},
|
||||
props: {
|
||||
execution: {
|
||||
type: Object as () => ExecutionSummary | null,
|
||||
type: Object as PropType<ExecutionSummary>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { ref, nextTick } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { Connection, ConnectionDetachedParams } from '@jsplumb/core';
|
||||
import { useHistoryStore } from '@/stores/history.store';
|
||||
import {
|
||||
CUSTOM_API_CALL_KEY,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
NODE_OUTPUT_DEFAULT_KEY,
|
||||
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
WEBHOOK_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
|
||||
import { NodeHelpers, NodeConnectionType, ExpressionEvaluatorProxy } from 'n8n-workflow';
|
||||
@@ -26,6 +32,10 @@ import type {
|
||||
INodeCredentialsDetails,
|
||||
INodeParameters,
|
||||
ITaskData,
|
||||
IConnections,
|
||||
INodeTypeNameVersion,
|
||||
IConnection,
|
||||
IPinData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type {
|
||||
@@ -43,12 +53,15 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
import { get } from 'lodash-es';
|
||||
import { useI18n } from './useI18n';
|
||||
import { EnableNodeToggleCommand } from '@/models/history';
|
||||
import { AddNodeCommand, EnableNodeToggleCommand, RemoveConnectionCommand } from '@/models/history';
|
||||
import { useTelemetry } from './useTelemetry';
|
||||
import { hasPermission } from '@/utils/rbac/permissions';
|
||||
import type { N8nPlusEndpoint } from '@/plugins/jsplumb/N8nPlusEndpointType';
|
||||
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
||||
import { useCanvasStore } from '@/stores/canvas.store';
|
||||
import { getEndpointScope } from '@/utils/nodeViewUtils';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { getConnectionInfo } from '@/utils/canvasUtils';
|
||||
|
||||
declare namespace HttpRequestNode {
|
||||
namespace V2 {
|
||||
@@ -67,6 +80,13 @@ export function useNodeHelpers() {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const i18n = useI18n();
|
||||
const canvasStore = useCanvasStore();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
const route = useRoute();
|
||||
|
||||
const isInsertingNodes = ref(false);
|
||||
const credentialsUpdated = ref(false);
|
||||
const isProductionExecutionPreview = ref(false);
|
||||
const pullConnActiveNodeName = ref<string | null>(null);
|
||||
|
||||
function hasProxyAuth(node: INodeUi): boolean {
|
||||
return Object.keys(node.parameters).includes('nodeCredentialType');
|
||||
@@ -776,6 +796,430 @@ export function useNodeHelpers() {
|
||||
});
|
||||
}
|
||||
|
||||
function matchCredentials(node: INodeUi) {
|
||||
if (!node.credentials) {
|
||||
return;
|
||||
}
|
||||
Object.entries(node.credentials).forEach(
|
||||
([nodeCredentialType, nodeCredentials]: [string, INodeCredentialsDetails]) => {
|
||||
const credentialOptions = credentialsStore.getCredentialsByType(nodeCredentialType);
|
||||
|
||||
// Check if workflows applies old credentials style
|
||||
if (typeof nodeCredentials === 'string') {
|
||||
nodeCredentials = {
|
||||
id: null,
|
||||
name: nodeCredentials,
|
||||
};
|
||||
credentialsUpdated.value = true;
|
||||
}
|
||||
|
||||
if (nodeCredentials.id) {
|
||||
// Check whether the id is matching with a credential
|
||||
const credentialsId = nodeCredentials.id.toString(); // due to a fixed bug in the migration UpdateWorkflowCredentials (just sqlite) we have to cast to string and check later if it has been a number
|
||||
const credentialsForId = credentialOptions.find(
|
||||
(optionData: ICredentialsResponse) => optionData.id === credentialsId,
|
||||
);
|
||||
if (credentialsForId) {
|
||||
if (
|
||||
credentialsForId.name !== nodeCredentials.name ||
|
||||
typeof nodeCredentials.id === 'number'
|
||||
) {
|
||||
node.credentials![nodeCredentialType] = {
|
||||
id: credentialsForId.id,
|
||||
name: credentialsForId.name,
|
||||
};
|
||||
credentialsUpdated.value = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No match for id found or old credentials type used
|
||||
node.credentials![nodeCredentialType] = nodeCredentials;
|
||||
|
||||
// check if only one option with the name would exist
|
||||
const credentialsForName = credentialOptions.filter(
|
||||
(optionData: ICredentialsResponse) => optionData.name === nodeCredentials.name,
|
||||
);
|
||||
|
||||
// only one option exists for the name, take it
|
||||
if (credentialsForName.length === 1) {
|
||||
node.credentials![nodeCredentialType].id = credentialsForName[0].id;
|
||||
credentialsUpdated.value = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function deleteJSPlumbConnection(connection: Connection, trackHistory = false) {
|
||||
// Make sure to remove the overlay else after the second move
|
||||
// it visibly stays behind free floating without a connection.
|
||||
connection.removeOverlays();
|
||||
|
||||
pullConnActiveNodeName.value = null; // prevent new connections when connectionDetached is triggered
|
||||
canvasStore.jsPlumbInstance?.deleteConnection(connection); // on delete, triggers connectionDetached event which applies mutation to store
|
||||
if (trackHistory && connection.__meta) {
|
||||
const connectionData: [IConnection, IConnection] = [
|
||||
{
|
||||
index: connection.__meta?.sourceOutputIndex,
|
||||
node: connection.__meta.sourceNodeName,
|
||||
type: NodeConnectionType.Main,
|
||||
},
|
||||
{
|
||||
index: connection.__meta?.targetOutputIndex,
|
||||
node: connection.__meta.targetNodeName,
|
||||
type: NodeConnectionType.Main,
|
||||
},
|
||||
];
|
||||
const removeCommand = new RemoveConnectionCommand(connectionData);
|
||||
historyStore.pushCommandToUndo(removeCommand);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise<void> {
|
||||
const allNodes: INodeTypeDescription[] = nodeTypesStore.allNodeTypes;
|
||||
|
||||
const nodesToBeFetched: INodeTypeNameVersion[] = [];
|
||||
allNodes.forEach((node) => {
|
||||
const nodeVersions = Array.isArray(node.version) ? node.version : [node.version];
|
||||
if (
|
||||
!!nodeInfos.find((n) => n.name === node.name && nodeVersions.includes(n.version)) &&
|
||||
!node.hasOwnProperty('properties')
|
||||
) {
|
||||
nodesToBeFetched.push({
|
||||
name: node.name,
|
||||
version: Array.isArray(node.version) ? node.version.slice(-1)[0] : node.version,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (nodesToBeFetched.length > 0) {
|
||||
// Only call API if node information is actually missing
|
||||
canvasStore.startLoading();
|
||||
await nodeTypesStore.getNodesInformation(nodesToBeFetched);
|
||||
canvasStore.stopLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function addConnectionsTestData() {
|
||||
canvasStore.jsPlumbInstance?.connections.forEach((connection) => {
|
||||
NodeViewUtils.addConnectionTestData(
|
||||
connection.source,
|
||||
connection.target,
|
||||
connection?.connector?.hasOwnProperty('canvas') ? connection?.connector.canvas : undefined,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function processConnectionBatch(batchedConnectionData: Array<[IConnection, IConnection]>) {
|
||||
const batchSize = 100;
|
||||
|
||||
for (let i = 0; i < batchedConnectionData.length; i += batchSize) {
|
||||
const batch = batchedConnectionData.slice(i, i + batchSize);
|
||||
|
||||
batch.forEach((connectionData) => {
|
||||
addConnection(connectionData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function addPinDataConnections(pinData?: IPinData) {
|
||||
if (!pinData) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(pinData).forEach((nodeName) => {
|
||||
const node = workflowsStore.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeElement = document.getElementById(node.id);
|
||||
if (!nodeElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasRun = workflowsStore.getWorkflowResultDataByNodeName(nodeName) !== null;
|
||||
// In case we are showing a production execution preview we want
|
||||
// to show pinned data connections as they wouldn't have been pinned
|
||||
const classNames = isProductionExecutionPreview.value ? [] : ['pinned'];
|
||||
|
||||
if (hasRun) {
|
||||
classNames.push('has-run');
|
||||
}
|
||||
|
||||
const connections = canvasStore.jsPlumbInstance?.getConnections({
|
||||
source: nodeElement,
|
||||
});
|
||||
|
||||
const connectionsArray = Array.isArray(connections)
|
||||
? connections
|
||||
: Object.values(connections);
|
||||
|
||||
connectionsArray.forEach((connection) => {
|
||||
NodeViewUtils.addConnectionOutputSuccess(connection, {
|
||||
total: pinData[nodeName].length,
|
||||
iterations: 0,
|
||||
classNames,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removePinDataConnections(pinData: IPinData) {
|
||||
Object.keys(pinData).forEach((nodeName) => {
|
||||
const node = workflowsStore.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeElement = document.getElementById(node.id);
|
||||
if (!nodeElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connections = canvasStore.jsPlumbInstance?.getConnections({
|
||||
source: nodeElement,
|
||||
});
|
||||
|
||||
const connectionsArray = Array.isArray(connections)
|
||||
? connections
|
||||
: Object.values(connections);
|
||||
|
||||
canvasStore.jsPlumbInstance.setSuspendDrawing(true);
|
||||
connectionsArray.forEach(NodeViewUtils.resetConnection);
|
||||
canvasStore.jsPlumbInstance.setSuspendDrawing(false, true);
|
||||
});
|
||||
}
|
||||
|
||||
function getOutputEndpointUUID(
|
||||
nodeName: string,
|
||||
connectionType: NodeConnectionType,
|
||||
index: number,
|
||||
): string | null {
|
||||
const node = workflowsStore.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return NodeViewUtils.getOutputEndpointUUID(node.id, connectionType, index);
|
||||
}
|
||||
function getInputEndpointUUID(
|
||||
nodeName: string,
|
||||
connectionType: NodeConnectionType,
|
||||
index: number,
|
||||
) {
|
||||
const node = workflowsStore.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return NodeViewUtils.getInputEndpointUUID(node.id, connectionType, index);
|
||||
}
|
||||
|
||||
function addConnection(connection: [IConnection, IConnection]) {
|
||||
const outputUuid = getOutputEndpointUUID(
|
||||
connection[0].node,
|
||||
connection[0].type,
|
||||
connection[0].index,
|
||||
);
|
||||
const inputUuid = getInputEndpointUUID(
|
||||
connection[1].node,
|
||||
connection[1].type,
|
||||
connection[1].index,
|
||||
);
|
||||
if (!outputUuid || !inputUuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uuids: [string, string] = [outputUuid, inputUuid];
|
||||
// Create connections in DOM
|
||||
canvasStore.jsPlumbInstance?.connect({
|
||||
uuids,
|
||||
detachable: !route?.meta?.readOnlyCanvas && !sourceControlStore.preferences.branchReadOnly,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
addPinDataConnections(workflowsStore.pinnedWorkflowData);
|
||||
});
|
||||
}
|
||||
|
||||
function removeConnection(
|
||||
connection: [IConnection, IConnection],
|
||||
removeVisualConnection = false,
|
||||
) {
|
||||
if (removeVisualConnection) {
|
||||
const sourceNode = workflowsStore.getNodeByName(connection[0].node);
|
||||
const targetNode = workflowsStore.getNodeByName(connection[1].node);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceElement = document.getElementById(sourceNode.id);
|
||||
const targetElement = document.getElementById(targetNode.id);
|
||||
|
||||
if (sourceElement && targetElement) {
|
||||
const connections = canvasStore.jsPlumbInstance?.getConnections({
|
||||
source: sourceElement,
|
||||
target: targetElement,
|
||||
});
|
||||
|
||||
if (Array.isArray(connections)) {
|
||||
connections.forEach((connectionInstance: Connection) => {
|
||||
if (connectionInstance.__meta) {
|
||||
// Only delete connections from specific indexes (if it can be determined by meta)
|
||||
if (
|
||||
connectionInstance.__meta.sourceOutputIndex === connection[0].index &&
|
||||
connectionInstance.__meta.targetOutputIndex === connection[1].index
|
||||
) {
|
||||
deleteJSPlumbConnection(connectionInstance);
|
||||
}
|
||||
} else {
|
||||
deleteJSPlumbConnection(connectionInstance);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
workflowsStore.removeConnection({ connection });
|
||||
}
|
||||
|
||||
function removeConnectionByConnectionInfo(
|
||||
info: ConnectionDetachedParams,
|
||||
removeVisualConnection = false,
|
||||
trackHistory = false,
|
||||
) {
|
||||
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
|
||||
|
||||
if (connectionInfo) {
|
||||
if (removeVisualConnection) {
|
||||
deleteJSPlumbConnection(info.connection, trackHistory);
|
||||
} else if (trackHistory) {
|
||||
historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo));
|
||||
}
|
||||
workflowsStore.removeConnection({ connection: connectionInfo });
|
||||
}
|
||||
}
|
||||
|
||||
async function addConnections(connections: IConnections) {
|
||||
const batchedConnectionData: Array<[IConnection, IConnection]> = [];
|
||||
|
||||
for (const sourceNode in connections) {
|
||||
for (const type in connections[sourceNode]) {
|
||||
connections[sourceNode][type].forEach((outwardConnections, sourceIndex) => {
|
||||
if (outwardConnections) {
|
||||
outwardConnections.forEach((targetData) => {
|
||||
batchedConnectionData.push([
|
||||
{
|
||||
node: sourceNode,
|
||||
type: getEndpointScope(type) ?? NodeConnectionType.Main,
|
||||
index: sourceIndex,
|
||||
},
|
||||
{ node: targetData.node, type: targetData.type, index: targetData.index },
|
||||
]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process the connections in batches
|
||||
await processConnectionBatch(batchedConnectionData);
|
||||
setTimeout(addConnectionsTestData, 0);
|
||||
}
|
||||
|
||||
async function addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) {
|
||||
if (!nodes?.length) {
|
||||
return;
|
||||
}
|
||||
isInsertingNodes.value = true;
|
||||
// Before proceeding we must check if all nodes contain the `properties` attribute.
|
||||
// Nodes are loaded without this information so we must make sure that all nodes
|
||||
// being added have this information.
|
||||
await loadNodesProperties(
|
||||
nodes.map((node) => ({ name: node.type, version: node.typeVersion })),
|
||||
);
|
||||
|
||||
// Add the node to the node-list
|
||||
let nodeType: INodeTypeDescription | null;
|
||||
nodes.forEach((node) => {
|
||||
const newNode: INodeUi = {
|
||||
...node,
|
||||
};
|
||||
|
||||
if (!newNode.id) {
|
||||
newNode.id = uuid();
|
||||
}
|
||||
|
||||
nodeType = nodeTypesStore.getNodeType(newNode.type, newNode.typeVersion);
|
||||
|
||||
// Make sure that some properties always exist
|
||||
if (!newNode.hasOwnProperty('disabled')) {
|
||||
newNode.disabled = false;
|
||||
}
|
||||
|
||||
if (!newNode.hasOwnProperty('parameters')) {
|
||||
newNode.parameters = {};
|
||||
}
|
||||
|
||||
// Load the default parameter values because only values which differ
|
||||
// from the defaults get saved
|
||||
if (nodeType !== null) {
|
||||
let nodeParameters = null;
|
||||
try {
|
||||
nodeParameters = NodeHelpers.getNodeParameters(
|
||||
nodeType.properties,
|
||||
newNode.parameters,
|
||||
true,
|
||||
false,
|
||||
node,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
i18n.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') +
|
||||
`: "${newNode.name}"`,
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
newNode.parameters = nodeParameters ?? {};
|
||||
|
||||
// if it's a webhook and the path is empty set the UUID as the default path
|
||||
if (
|
||||
[WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE].includes(newNode.type) &&
|
||||
newNode.parameters.path === ''
|
||||
) {
|
||||
newNode.parameters.path = newNode.webhookId as string;
|
||||
}
|
||||
}
|
||||
|
||||
// check and match credentials, apply new format if old is used
|
||||
matchCredentials(newNode);
|
||||
workflowsStore.addNode(newNode);
|
||||
if (trackHistory) {
|
||||
historyStore.pushCommandToUndo(new AddNodeCommand(newNode));
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the nodes to be rendered
|
||||
await nextTick();
|
||||
|
||||
canvasStore.jsPlumbInstance?.setSuspendDrawing(true);
|
||||
|
||||
if (connections) {
|
||||
await addConnections(connections);
|
||||
}
|
||||
// Add the node issues at the end as the node-connections are required
|
||||
refreshNodeIssues();
|
||||
updateNodesInputIssues();
|
||||
/////////////////////////////this.resetEndpointsErrors();
|
||||
isInsertingNodes.value = false;
|
||||
|
||||
// Now it can draw again
|
||||
canvasStore.jsPlumbInstance?.setSuspendDrawing(false, true);
|
||||
}
|
||||
|
||||
return {
|
||||
hasProxyAuth,
|
||||
isCustomApiCallSelected,
|
||||
@@ -795,5 +1239,17 @@ export function useNodeHelpers() {
|
||||
updateNodesCredentialsIssues,
|
||||
getNodeInputData,
|
||||
setSuccessOutput,
|
||||
isInsertingNodes,
|
||||
credentialsUpdated,
|
||||
isProductionExecutionPreview,
|
||||
pullConnActiveNodeName,
|
||||
deleteJSPlumbConnection,
|
||||
loadNodesProperties,
|
||||
addNodes,
|
||||
addConnection,
|
||||
removeConnection,
|
||||
removeConnectionByConnectionInfo,
|
||||
addPinDataConnections,
|
||||
removePinDataConnections,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ import { useI18n } from '@/composables/useI18n';
|
||||
import type { useRouter } from 'vue-router';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { useTagsStore } from '@/stores/tags.store';
|
||||
|
||||
export function resolveParameter<T = IDataObject>(
|
||||
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
|
||||
@@ -483,6 +484,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
||||
const uiStore = useUIStore();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const projectsStore = useProjectsStore();
|
||||
const tagsStore = useTagsStore();
|
||||
|
||||
const toast = useToast();
|
||||
const message = useMessage();
|
||||
@@ -1189,6 +1191,25 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
||||
}
|
||||
}
|
||||
|
||||
async function initState(workflowData: IWorkflowDb): Promise<void> {
|
||||
workflowsStore.addWorkflow(workflowData);
|
||||
workflowsStore.setActive(workflowData.active || false);
|
||||
workflowsStore.setWorkflowId(workflowData.id);
|
||||
workflowsStore.setWorkflowName({
|
||||
newName: workflowData.name,
|
||||
setStateDirty: uiStore.stateIsDirty,
|
||||
});
|
||||
workflowsStore.setWorkflowSettings(workflowData.settings ?? {});
|
||||
workflowsStore.setWorkflowPinData(workflowData.pinData ?? {});
|
||||
workflowsStore.setWorkflowVersionId(workflowData.versionId);
|
||||
workflowsStore.setWorkflowMetadata(workflowData.meta);
|
||||
|
||||
const tags = (workflowData.tags ?? []) as ITag[];
|
||||
const tagIds = tags.map((tag) => tag.id);
|
||||
workflowsStore.setWorkflowTagIds(tagIds || []);
|
||||
tagsStore.upsertTags(tags);
|
||||
}
|
||||
|
||||
return {
|
||||
resolveParameter,
|
||||
resolveRequiredParameters,
|
||||
@@ -1214,5 +1235,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
||||
dataHasChanged,
|
||||
removeForeignCredentialsFromWorkflow,
|
||||
getWorkflowProjectRole,
|
||||
initState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,8 +73,6 @@ import { i18n } from '@/plugins/i18n';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { useSettingsStore } from './settings.store';
|
||||
import { useUsersStore } from './users.store';
|
||||
|
||||
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
|
||||
name: '',
|
||||
@@ -101,6 +99,8 @@ let cachedWorkflowKey: string | null = '';
|
||||
let cachedWorkflow: Workflow | null = null;
|
||||
|
||||
export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
const uiStore = useUIStore();
|
||||
|
||||
const workflow = ref<IWorkflowDb>(createEmptyWorkflow());
|
||||
const usedCredentials = ref<Record<string, IUsedCredential>>({});
|
||||
|
||||
@@ -436,8 +436,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
}
|
||||
|
||||
function resetWorkflow() {
|
||||
const usersStore = useUsersStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
workflow.value = createEmptyWorkflow();
|
||||
}
|
||||
|
||||
@@ -481,7 +479,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
|
||||
function setWorkflowName(data: { newName: string; setStateDirty: boolean }) {
|
||||
if (data.setStateDirty) {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
}
|
||||
workflow.value.name = data.newName;
|
||||
@@ -560,7 +557,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
}
|
||||
|
||||
function setWorkflowActive(targetWorkflowId: string) {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = false;
|
||||
const index = activeWorkflows.value.indexOf(targetWorkflowId);
|
||||
if (index === -1) {
|
||||
@@ -648,10 +644,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
}
|
||||
|
||||
function setWorkflowTagIds(tags: string[]) {
|
||||
workflow.value = {
|
||||
...workflow.value,
|
||||
tags,
|
||||
};
|
||||
workflow.value.tags = tags;
|
||||
}
|
||||
|
||||
function addWorkflowTagIds(tags: string[]) {
|
||||
@@ -715,7 +708,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
},
|
||||
};
|
||||
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
dataPinningEventBus.emit('pin-data', { [payload.node.name]: storedPinData });
|
||||
@@ -732,7 +724,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
pinData,
|
||||
};
|
||||
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
dataPinningEventBus.emit('unpin-data', { [payload.node.name]: undefined });
|
||||
@@ -829,7 +820,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
const connections =
|
||||
@@ -847,8 +837,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
}
|
||||
|
||||
function removeAllConnections(data: { setStateDirty: boolean }): void {
|
||||
if (data && data.setStateDirty) {
|
||||
const uiStore = useUIStore();
|
||||
if (data?.setStateDirty) {
|
||||
uiStore.stateIsDirty = true;
|
||||
}
|
||||
|
||||
@@ -859,7 +848,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
node: INodeUi,
|
||||
{ preserveInputConnections = false, preserveOutputConnections = false } = {},
|
||||
): void {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
// Remove all source connections
|
||||
@@ -904,7 +892,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
}
|
||||
|
||||
function renameNodeSelectedAndExecution(nameData: { old: string; new: string }): void {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
// If node has any WorkflowResultData rename also that one that the data
|
||||
@@ -1024,7 +1011,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
}
|
||||
|
||||
function removeNode(node: INodeUi): void {
|
||||
const uiStore = useUIStore();
|
||||
const { [node.name]: removedNodeMetadata, ...remainingNodeMetadata } = nodeMetadata.value;
|
||||
nodeMetadata.value = remainingNodeMetadata;
|
||||
|
||||
@@ -1051,7 +1037,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
|
||||
function removeAllNodes(data: { setStateDirty: boolean; removePinData: boolean }): void {
|
||||
if (data.setStateDirty) {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
}
|
||||
|
||||
@@ -1074,7 +1059,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
|
||||
if (nodeIndex !== -1) {
|
||||
for (const key of Object.keys(updateInformation.properties)) {
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
updateNodeAtIndex(nodeIndex, {
|
||||
@@ -1096,7 +1080,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
);
|
||||
}
|
||||
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
updateNodeAtIndex(nodeIndex, {
|
||||
@@ -1118,7 +1101,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
|
||||
const node = workflow.value.nodes[nodeIndex];
|
||||
|
||||
const uiStore = useUIStore();
|
||||
uiStore.stateIsDirty = true;
|
||||
const newParameters =
|
||||
!!append && isObject(updateInformation.value)
|
||||
|
||||
@@ -235,8 +235,6 @@ import {
|
||||
START_NODE_TYPE,
|
||||
STICKY_NODE_TYPE,
|
||||
VIEWS,
|
||||
WEBHOOK_NODE_TYPE,
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
TRIGGER_NODE_CREATOR_VIEW,
|
||||
EnterpriseEditionFeature,
|
||||
REGULAR_NODE_CREATOR_VIEW,
|
||||
@@ -279,11 +277,8 @@ import type {
|
||||
ExecutionSummary,
|
||||
INode,
|
||||
INodeConnections,
|
||||
INodeCredentialsDetails,
|
||||
INodeInputConfiguration,
|
||||
INodeTypeDescription,
|
||||
INodeTypeNameVersion,
|
||||
IPinData,
|
||||
ITaskData,
|
||||
ITelemetryTrackProperties,
|
||||
IWorkflowBase,
|
||||
@@ -302,7 +297,6 @@ import {
|
||||
} from 'n8n-workflow';
|
||||
import type {
|
||||
NewConnectionInfo,
|
||||
ICredentialsResponse,
|
||||
IExecutionResponse,
|
||||
IWorkflowDb,
|
||||
IWorkflowData,
|
||||
@@ -350,7 +344,6 @@ import { getAccountAge } from '@/utils/userUtils';
|
||||
import { getConnectionInfo, getNodeViewTab } from '@/utils/canvasUtils';
|
||||
import {
|
||||
AddConnectionCommand,
|
||||
AddNodeCommand,
|
||||
MoveNodeCommand,
|
||||
RemoveConnectionCommand,
|
||||
RemoveNodeCommand,
|
||||
@@ -550,8 +543,6 @@ export default defineComponent({
|
||||
moveCanvasKeyPressed: false,
|
||||
stopExecutionInProgress: false,
|
||||
blankRedirect: false,
|
||||
credentialsUpdated: false,
|
||||
pullConnActiveNodeName: null as string | null,
|
||||
pullConnActive: false,
|
||||
dropPrevented: false,
|
||||
connectionDragScope: {
|
||||
@@ -564,8 +555,6 @@ export default defineComponent({
|
||||
showTriggerMissingTooltip: false,
|
||||
workflowData: null as INewWorkflowData | null,
|
||||
activeConnection: null as null | Connection,
|
||||
isInsertingNodes: false,
|
||||
isProductionExecutionPreview: false,
|
||||
enterTimer: undefined as undefined | ReturnType<typeof setTimeout>,
|
||||
exitTimer: undefined as undefined | ReturnType<typeof setTimeout>,
|
||||
readOnlyNotification: null as null | NotificationHandle,
|
||||
@@ -779,6 +768,9 @@ export default defineComponent({
|
||||
const isCloudDeployment = this.settingsStore.isCloudDeployment;
|
||||
return isCloudDeployment && experimentEnabled && !userHasSeenAIAssistantExperiment;
|
||||
},
|
||||
isProductionExecutionPreview(): boolean {
|
||||
return this.nodeHelpers.isProductionExecutionPreview.value;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// Listen to route changes and load the workflow accordingly
|
||||
@@ -916,7 +908,7 @@ export default defineComponent({
|
||||
|
||||
setTimeout(() => {
|
||||
void this.usersStore.showPersonalizationSurvey();
|
||||
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
|
||||
this.nodeHelpers.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
@@ -1007,8 +999,8 @@ export default defineComponent({
|
||||
historyBus.on('revertRenameNode', this.onRevertNameChange);
|
||||
historyBus.on('enableNodeToggle', this.onRevertEnableToggle);
|
||||
|
||||
dataPinningEventBus.on('pin-data', this.addPinDataConnections);
|
||||
dataPinningEventBus.on('unpin-data', this.removePinDataConnections);
|
||||
dataPinningEventBus.on('pin-data', this.nodeHelpers.addPinDataConnections);
|
||||
dataPinningEventBus.on('unpin-data', this.nodeHelpers.removePinDataConnections);
|
||||
nodeViewEventBus.on('saveWorkflow', this.saveCurrentWorkflowExternal);
|
||||
|
||||
this.canvasStore.isDemo = this.isDemo;
|
||||
@@ -1033,8 +1025,8 @@ export default defineComponent({
|
||||
historyBus.off('revertRenameNode', this.onRevertNameChange);
|
||||
historyBus.off('enableNodeToggle', this.onRevertEnableToggle);
|
||||
|
||||
dataPinningEventBus.off('pin-data', this.addPinDataConnections);
|
||||
dataPinningEventBus.off('unpin-data', this.removePinDataConnections);
|
||||
dataPinningEventBus.off('pin-data', this.nodeHelpers.addPinDataConnections);
|
||||
dataPinningEventBus.off('unpin-data', this.nodeHelpers.removePinDataConnections);
|
||||
nodeViewEventBus.off('saveWorkflow', this.saveCurrentWorkflowExternal);
|
||||
},
|
||||
beforeMount() {
|
||||
@@ -1334,7 +1326,7 @@ export default defineComponent({
|
||||
this.workflowsStore.setUsedCredentials(data.workflowData.usedCredentials);
|
||||
}
|
||||
|
||||
await this.addNodes(
|
||||
await this.nodeHelpers.addNodes(
|
||||
deepCopy(data.workflowData.nodes),
|
||||
deepCopy(data.workflowData.connections),
|
||||
);
|
||||
@@ -1405,7 +1397,7 @@ export default defineComponent({
|
||||
this.resetWorkspace();
|
||||
data.workflow.nodes = NodeViewUtils.getFixedNodesList(data.workflow.nodes);
|
||||
|
||||
await this.addNodes(data.workflow.nodes as INodeUi[], data.workflow.connections);
|
||||
await this.nodeHelpers.addNodes(data.workflow.nodes as INodeUi[], data.workflow.connections);
|
||||
|
||||
if (data.workflow.pinData) {
|
||||
this.workflowsStore.setWorkflowPinData(data.workflow.pinData);
|
||||
@@ -1457,7 +1449,7 @@ export default defineComponent({
|
||||
const convertedNodes = data.workflow.nodes.map(
|
||||
this.workflowsStore.convertTemplateNodeToNodeUi,
|
||||
);
|
||||
await this.addNodes(convertedNodes, data.workflow.connections);
|
||||
await this.nodeHelpers.addNodes(convertedNodes, data.workflow.connections);
|
||||
this.workflowData =
|
||||
(await this.workflowsStore.getNewWorkflowData(
|
||||
data.name,
|
||||
@@ -1482,14 +1474,7 @@ export default defineComponent({
|
||||
|
||||
this.resetWorkspace();
|
||||
|
||||
this.workflowsStore.addWorkflow(workflow);
|
||||
this.workflowsStore.setActive(workflow.active || false);
|
||||
this.workflowsStore.setWorkflowId(workflow.id);
|
||||
this.workflowsStore.setWorkflowName({ newName: workflow.name, setStateDirty: false });
|
||||
this.workflowsStore.setWorkflowSettings(workflow.settings ?? {});
|
||||
this.workflowsStore.setWorkflowPinData(workflow.pinData ?? {});
|
||||
this.workflowsStore.setWorkflowVersionId(workflow.versionId);
|
||||
this.workflowsStore.setWorkflowMetadata(workflow.meta);
|
||||
await this.workflowHelpers.initState(workflow);
|
||||
|
||||
if (workflow.sharedWithProjects) {
|
||||
this.workflowsEEStore.setWorkflowSharedWith({
|
||||
@@ -1502,14 +1487,9 @@ export default defineComponent({
|
||||
this.workflowsStore.setUsedCredentials(workflow.usedCredentials);
|
||||
}
|
||||
|
||||
const tags = (workflow.tags ?? []) as ITag[];
|
||||
const tagIds = tags.map((tag) => tag.id);
|
||||
this.workflowsStore.setWorkflowTagIds(tagIds || []);
|
||||
this.tagsStore.upsertTags(tags);
|
||||
await this.nodeHelpers.addNodes(workflow.nodes, workflow.connections);
|
||||
|
||||
await this.addNodes(workflow.nodes, workflow.connections);
|
||||
|
||||
if (!this.credentialsUpdated) {
|
||||
if (!this.nodeHelpers.credentialsUpdated.value) {
|
||||
this.uiStore.stateIsDirty = false;
|
||||
}
|
||||
this.canvasStore.zoomToFit();
|
||||
@@ -2340,7 +2320,7 @@ export default defineComponent({
|
||||
|
||||
this.workflowsStore.addWorkflowTagIds(tagIds);
|
||||
setTimeout(() => {
|
||||
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
|
||||
this.nodeHelpers.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -2461,7 +2441,7 @@ export default defineComponent({
|
||||
[defaultCredential.type]: selected,
|
||||
};
|
||||
|
||||
await this.loadNodesProperties(
|
||||
await this.nodeHelpers.loadNodesProperties(
|
||||
[newNodeData].map((node) => ({ name: node.type, version: node.typeVersion })),
|
||||
);
|
||||
const nodeType = this.nodeTypesStore.getNodeType(newNodeData.type, newNodeData.typeVersion);
|
||||
@@ -2703,7 +2683,7 @@ export default defineComponent({
|
||||
newNodeData.webhookId = uuid();
|
||||
}
|
||||
|
||||
await this.addNodes([newNodeData], undefined, trackHistory);
|
||||
await this.nodeHelpers.addNodes([newNodeData], undefined, trackHistory);
|
||||
this.workflowsStore.setNodePristine(newNodeData.name, true);
|
||||
|
||||
this.uiStore.stateIsDirty = true;
|
||||
@@ -2811,7 +2791,7 @@ export default defineComponent({
|
||||
},
|
||||
] as [IConnection, IConnection];
|
||||
|
||||
this.__addConnection(connectionData);
|
||||
this.nodeHelpers.addConnection(connectionData);
|
||||
},
|
||||
async addNode(
|
||||
nodeTypeName: string,
|
||||
@@ -2879,7 +2859,7 @@ export default defineComponent({
|
||||
await this.$nextTick();
|
||||
|
||||
if (lastSelectedConnection?.__meta) {
|
||||
this.__deleteJSPlumbConnection(lastSelectedConnection, trackHistory);
|
||||
this.nodeHelpers.deleteJSPlumbConnection(lastSelectedConnection, trackHistory);
|
||||
|
||||
const targetNodeName = lastSelectedConnection.__meta.targetNodeName;
|
||||
const targetOutputIndex = lastSelectedConnection.__meta.targetOutputIndex;
|
||||
@@ -2980,7 +2960,7 @@ export default defineComponent({
|
||||
this.dropPrevented = false;
|
||||
return;
|
||||
}
|
||||
if (this.pullConnActiveNodeName) {
|
||||
if (this.nodeHelpers.pullConnActiveNodeName.value) {
|
||||
const sourceNode = this.workflowsStore.getNodeById(connection.parameters.nodeId);
|
||||
const connectionType = connection.parameters.type ?? NodeConnectionType.Main;
|
||||
const overrideTargetEndpoint = connection?.connector
|
||||
@@ -2988,8 +2968,12 @@ export default defineComponent({
|
||||
|
||||
if (sourceNode) {
|
||||
const isTarget = connection.parameters.connection === 'target';
|
||||
const sourceNodeName = isTarget ? this.pullConnActiveNodeName : sourceNode.name;
|
||||
const targetNodeName = isTarget ? sourceNode.name : this.pullConnActiveNodeName;
|
||||
const sourceNodeName = isTarget
|
||||
? this.nodeHelpers.pullConnActiveNodeName.value
|
||||
: sourceNode.name;
|
||||
const targetNodeName = isTarget
|
||||
? sourceNode.name
|
||||
: this.nodeHelpers.pullConnActiveNodeName.value;
|
||||
const outputIndex = connection.parameters.index;
|
||||
NodeViewUtils.resetConnectionAfterPull(connection);
|
||||
await this.$nextTick();
|
||||
@@ -3001,7 +2985,7 @@ export default defineComponent({
|
||||
overrideTargetEndpoint?.parameters?.index ?? 0,
|
||||
connectionType,
|
||||
);
|
||||
this.pullConnActiveNodeName = null;
|
||||
this.nodeHelpers.pullConnActiveNodeName.value = null;
|
||||
this.dropPrevented = false;
|
||||
}
|
||||
return;
|
||||
@@ -3149,7 +3133,7 @@ export default defineComponent({
|
||||
)
|
||||
) {
|
||||
this.dropPrevented = true;
|
||||
this.pullConnActiveNodeName = null;
|
||||
this.nodeHelpers.pullConnActiveNodeName.value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3213,7 +3197,7 @@ export default defineComponent({
|
||||
info.connection,
|
||||
() => {
|
||||
this.activeConnection = null;
|
||||
this.__deleteJSPlumbConnection(info.connection);
|
||||
this.nodeHelpers.deleteJSPlumbConnection(info.connection);
|
||||
},
|
||||
() => {
|
||||
this.insertNodeAfterSelected({
|
||||
@@ -3245,7 +3229,7 @@ export default defineComponent({
|
||||
}
|
||||
// When we add multiple nodes, this event could be fired hundreds of times for large workflows.
|
||||
// And because the updateNodesInputIssues() method is quite expensive, we only call it if not in insert mode
|
||||
if (!this.isInsertingNodes) {
|
||||
if (!this.nodeHelpers.isInsertingNodes.value) {
|
||||
this.nodeHelpers.updateNodesInputIssues();
|
||||
this.resetEndpointsErrors();
|
||||
setTimeout(() => {
|
||||
@@ -3263,17 +3247,6 @@ export default defineComponent({
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
addConectionsTestData() {
|
||||
this.instance.connections.forEach((connection) => {
|
||||
NodeViewUtils.addConnectionTestData(
|
||||
connection.source,
|
||||
connection.target,
|
||||
connection?.connector?.hasOwnProperty('canvas')
|
||||
? connection?.connector.canvas
|
||||
: undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
onDragMove() {
|
||||
const totalNodes = this.nodes.length;
|
||||
void this.callDebounced(this.updateConnectionsOverlays, {
|
||||
@@ -3390,7 +3363,7 @@ export default defineComponent({
|
||||
},
|
||||
] as [IConnection, IConnection];
|
||||
|
||||
this.__removeConnection(connectionInfo, false);
|
||||
this.nodeHelpers.removeConnection(connectionInfo, false);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -3420,9 +3393,9 @@ export default defineComponent({
|
||||
NodeViewUtils.showOutputNameLabel(info.sourceEndpoint, info.connection);
|
||||
|
||||
info.connection.removeOverlays();
|
||||
this.__removeConnectionByConnectionInfo(info, false, false);
|
||||
this.nodeHelpers.removeConnectionByConnectionInfo(info, false, false);
|
||||
|
||||
if (this.pullConnActiveNodeName) {
|
||||
if (this.nodeHelpers.pullConnActiveNodeName.value) {
|
||||
// establish new connection when dragging connection from one node to another
|
||||
this.historyStore.startRecordingUndo();
|
||||
const sourceNode = this.workflowsStore.getNodeById(info.connection.parameters.nodeId);
|
||||
@@ -3442,11 +3415,11 @@ export default defineComponent({
|
||||
this.connectTwoNodes(
|
||||
sourceNodeName,
|
||||
outputIndex,
|
||||
this.pullConnActiveNodeName,
|
||||
this.nodeHelpers.pullConnActiveNodeName.value,
|
||||
overrideTargetEndpoint?.parameters?.index ?? 0,
|
||||
NodeConnectionType.Main,
|
||||
);
|
||||
this.pullConnActiveNodeName = null;
|
||||
this.nodeHelpers.pullConnActiveNodeName.value = null;
|
||||
await this.$nextTick();
|
||||
this.historyStore.stopRecordingUndo();
|
||||
} else if (
|
||||
@@ -3470,7 +3443,7 @@ export default defineComponent({
|
||||
// manually
|
||||
connection.overlays['midpoint-arrow']?.setVisible(false);
|
||||
try {
|
||||
this.pullConnActiveNodeName = null;
|
||||
this.nodeHelpers.pullConnActiveNodeName.value = null;
|
||||
this.pullConnActive = true;
|
||||
this.canvasStore.newNodeInsertPosition = null;
|
||||
NodeViewUtils.hideConnectionActions(connection);
|
||||
@@ -3555,12 +3528,12 @@ export default defineComponent({
|
||||
const endpoint = intersectingEndpoint.jtk.endpoint as Endpoint;
|
||||
const node = this.workflowsStore.getNodeById(endpoint.parameters.nodeId);
|
||||
|
||||
this.pullConnActiveNodeName = node?.name ?? null;
|
||||
this.nodeHelpers.pullConnActiveNodeName.value = node?.name ?? null;
|
||||
|
||||
NodeViewUtils.showDropConnectionState(connection, endpoint);
|
||||
} else {
|
||||
NodeViewUtils.showPullConnectionState(connection);
|
||||
this.pullConnActiveNodeName = null;
|
||||
this.nodeHelpers.pullConnActiveNodeName.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3762,7 +3735,7 @@ export default defineComponent({
|
||||
) {
|
||||
const manualTriggerNode = this.canvasStore.getAutoAddManualTriggerNode();
|
||||
if (manualTriggerNode) {
|
||||
await this.addNodes([manualTriggerNode]);
|
||||
await this.nodeHelpers.addNodes([manualTriggerNode]);
|
||||
this.uiStore.lastSelectedNode = manualTriggerNode.name;
|
||||
}
|
||||
}
|
||||
@@ -3857,130 +3830,6 @@ export default defineComponent({
|
||||
// waiting in the store and display them
|
||||
this.showNotificationForViews([VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW]);
|
||||
},
|
||||
getOutputEndpointUUID(
|
||||
nodeName: string,
|
||||
connectionType: NodeConnectionType,
|
||||
index: number,
|
||||
): string | null {
|
||||
const node = this.workflowsStore.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return NodeViewUtils.getOutputEndpointUUID(node.id, connectionType, index);
|
||||
},
|
||||
getInputEndpointUUID(nodeName: string, connectionType: NodeConnectionType, index: number) {
|
||||
const node = this.workflowsStore.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return NodeViewUtils.getInputEndpointUUID(node.id, connectionType, index);
|
||||
},
|
||||
__addConnection(connection: [IConnection, IConnection]) {
|
||||
const outputUuid = this.getOutputEndpointUUID(
|
||||
connection[0].node,
|
||||
connection[0].type,
|
||||
connection[0].index,
|
||||
);
|
||||
const inputUuid = this.getInputEndpointUUID(
|
||||
connection[1].node,
|
||||
connection[1].type,
|
||||
connection[1].index,
|
||||
);
|
||||
if (!outputUuid || !inputUuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uuid: [string, string] = [outputUuid, inputUuid];
|
||||
// Create connections in DOM
|
||||
this.instance?.connect({
|
||||
uuids: uuid,
|
||||
detachable: !this.isReadOnlyRoute && !this.readOnlyEnv,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
|
||||
});
|
||||
},
|
||||
__removeConnection(connection: [IConnection, IConnection], removeVisualConnection = false) {
|
||||
if (removeVisualConnection) {
|
||||
const sourceNode = this.workflowsStore.getNodeByName(connection[0].node);
|
||||
const targetNode = this.workflowsStore.getNodeByName(connection[1].node);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceElement = document.getElementById(sourceNode.id);
|
||||
const targetElement = document.getElementById(targetNode.id);
|
||||
|
||||
if (sourceElement && targetElement) {
|
||||
const connections = this.instance?.getConnections({
|
||||
source: sourceElement,
|
||||
target: targetElement,
|
||||
});
|
||||
|
||||
if (Array.isArray(connections)) {
|
||||
connections.forEach((connectionInstance: Connection) => {
|
||||
if (connectionInstance.__meta) {
|
||||
// Only delete connections from specific indexes (if it can be determined by meta)
|
||||
if (
|
||||
connectionInstance.__meta.sourceOutputIndex === connection[0].index &&
|
||||
connectionInstance.__meta.targetOutputIndex === connection[1].index
|
||||
) {
|
||||
this.__deleteJSPlumbConnection(connectionInstance);
|
||||
}
|
||||
} else {
|
||||
this.__deleteJSPlumbConnection(connectionInstance);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.workflowsStore.removeConnection({ connection });
|
||||
},
|
||||
__deleteJSPlumbConnection(connection: Connection, trackHistory = false) {
|
||||
// Make sure to remove the overlay else after the second move
|
||||
// it visibly stays behind free floating without a connection.
|
||||
connection.removeOverlays();
|
||||
|
||||
this.pullConnActiveNodeName = null; // prevent new connections when connectionDetached is triggered
|
||||
this.instance?.deleteConnection(connection); // on delete, triggers connectionDetached event which applies mutation to store
|
||||
if (trackHistory && connection.__meta) {
|
||||
const connectionData: [IConnection, IConnection] = [
|
||||
{
|
||||
index: connection.__meta?.sourceOutputIndex,
|
||||
node: connection.__meta.sourceNodeName,
|
||||
type: NodeConnectionType.Main,
|
||||
},
|
||||
{
|
||||
index: connection.__meta?.targetOutputIndex,
|
||||
node: connection.__meta.targetNodeName,
|
||||
type: NodeConnectionType.Main,
|
||||
},
|
||||
];
|
||||
const removeCommand = new RemoveConnectionCommand(connectionData);
|
||||
this.historyStore.pushCommandToUndo(removeCommand);
|
||||
}
|
||||
},
|
||||
__removeConnectionByConnectionInfo(
|
||||
info: ConnectionDetachedParams,
|
||||
removeVisualConnection = false,
|
||||
trackHistory = false,
|
||||
) {
|
||||
const connectionInfo: [IConnection, IConnection] | null = getConnectionInfo(info);
|
||||
|
||||
if (connectionInfo) {
|
||||
if (removeVisualConnection) {
|
||||
this.__deleteJSPlumbConnection(info.connection, trackHistory);
|
||||
} else if (trackHistory) {
|
||||
this.historyStore.pushCommandToUndo(new RemoveConnectionCommand(connectionInfo));
|
||||
}
|
||||
this.workflowsStore.removeConnection({ connection: connectionInfo });
|
||||
}
|
||||
},
|
||||
async duplicateNodes(nodes: INode[]): Promise<void> {
|
||||
if (!this.editAllowedCheck()) {
|
||||
return;
|
||||
@@ -4310,7 +4159,11 @@ export default defineComponent({
|
||||
await this.$nextTick();
|
||||
|
||||
// Add the new updated nodes
|
||||
await this.addNodes(Object.values(workflow.nodes), workflow.connectionsBySourceNode, false);
|
||||
await this.nodeHelpers.addNodes(
|
||||
Object.values(workflow.nodes),
|
||||
workflow.connectionsBySourceNode,
|
||||
false,
|
||||
);
|
||||
|
||||
// Make sure that the node is selected again
|
||||
this.deselectAllNodes();
|
||||
@@ -4337,188 +4190,6 @@ export default defineComponent({
|
||||
this.instance.deleteEveryConnection({ fireEvent: true });
|
||||
}
|
||||
},
|
||||
matchCredentials(node: INodeUi) {
|
||||
if (!node.credentials) {
|
||||
return;
|
||||
}
|
||||
Object.entries(node.credentials).forEach(
|
||||
([nodeCredentialType, nodeCredentials]: [string, INodeCredentialsDetails]) => {
|
||||
const credentialOptions = this.credentialsStore.getCredentialsByType(nodeCredentialType);
|
||||
|
||||
// Check if workflows applies old credentials style
|
||||
if (typeof nodeCredentials === 'string') {
|
||||
nodeCredentials = {
|
||||
id: null,
|
||||
name: nodeCredentials,
|
||||
};
|
||||
this.credentialsUpdated = true;
|
||||
}
|
||||
|
||||
if (nodeCredentials.id) {
|
||||
// Check whether the id is matching with a credential
|
||||
const credentialsId = nodeCredentials.id.toString(); // due to a fixed bug in the migration UpdateWorkflowCredentials (just sqlite) we have to cast to string and check later if it has been a number
|
||||
const credentialsForId = credentialOptions.find(
|
||||
(optionData: ICredentialsResponse) => optionData.id === credentialsId,
|
||||
);
|
||||
if (credentialsForId) {
|
||||
if (
|
||||
credentialsForId.name !== nodeCredentials.name ||
|
||||
typeof nodeCredentials.id === 'number'
|
||||
) {
|
||||
node.credentials![nodeCredentialType] = {
|
||||
id: credentialsForId.id,
|
||||
name: credentialsForId.name,
|
||||
};
|
||||
this.credentialsUpdated = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No match for id found or old credentials type used
|
||||
node.credentials![nodeCredentialType] = nodeCredentials;
|
||||
|
||||
// check if only one option with the name would exist
|
||||
const credentialsForName = credentialOptions.filter(
|
||||
(optionData: ICredentialsResponse) => optionData.name === nodeCredentials.name,
|
||||
);
|
||||
|
||||
// only one option exists for the name, take it
|
||||
if (credentialsForName.length === 1) {
|
||||
node.credentials![nodeCredentialType].id = credentialsForName[0].id;
|
||||
this.credentialsUpdated = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
async addNodes(nodes: INodeUi[], connections?: IConnections, trackHistory = false) {
|
||||
if (!nodes?.length) {
|
||||
return;
|
||||
}
|
||||
this.isInsertingNodes = true;
|
||||
// Before proceeding we must check if all nodes contain the `properties` attribute.
|
||||
// Nodes are loaded without this information so we must make sure that all nodes
|
||||
// being added have this information.
|
||||
await this.loadNodesProperties(
|
||||
nodes.map((node) => ({ name: node.type, version: node.typeVersion })),
|
||||
);
|
||||
|
||||
// Add the node to the node-list
|
||||
let nodeType: INodeTypeDescription | null;
|
||||
nodes.forEach((node) => {
|
||||
const newNode: INodeUi = {
|
||||
...node,
|
||||
};
|
||||
|
||||
if (!newNode.id) {
|
||||
newNode.id = uuid();
|
||||
}
|
||||
|
||||
nodeType = this.nodeTypesStore.getNodeType(newNode.type, newNode.typeVersion);
|
||||
|
||||
// Make sure that some properties always exist
|
||||
if (!newNode.hasOwnProperty('disabled')) {
|
||||
newNode.disabled = false;
|
||||
}
|
||||
|
||||
if (!newNode.hasOwnProperty('parameters')) {
|
||||
newNode.parameters = {};
|
||||
}
|
||||
|
||||
// Load the default parameter values because only values which differ
|
||||
// from the defaults get saved
|
||||
if (nodeType !== null) {
|
||||
let nodeParameters = null;
|
||||
try {
|
||||
nodeParameters = NodeHelpers.getNodeParameters(
|
||||
nodeType.properties,
|
||||
newNode.parameters,
|
||||
true,
|
||||
false,
|
||||
node,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(
|
||||
this.$locale.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') +
|
||||
`: "${newNode.name}"`,
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
newNode.parameters = nodeParameters ?? {};
|
||||
|
||||
// if it's a webhook and the path is empty set the UUID as the default path
|
||||
if (
|
||||
[WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE].includes(newNode.type) &&
|
||||
newNode.parameters.path === ''
|
||||
) {
|
||||
newNode.parameters.path = newNode.webhookId as string;
|
||||
}
|
||||
}
|
||||
|
||||
// check and match credentials, apply new format if old is used
|
||||
this.matchCredentials(newNode);
|
||||
this.workflowsStore.addNode(newNode);
|
||||
if (trackHistory) {
|
||||
this.historyStore.pushCommandToUndo(new AddNodeCommand(newNode));
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for the nodes to be rendered
|
||||
await this.$nextTick();
|
||||
|
||||
this.instance?.setSuspendDrawing(true);
|
||||
|
||||
if (connections) {
|
||||
await this.addConnections(connections);
|
||||
}
|
||||
// Add the node issues at the end as the node-connections are required
|
||||
this.nodeHelpers.refreshNodeIssues();
|
||||
this.nodeHelpers.updateNodesInputIssues();
|
||||
this.resetEndpointsErrors();
|
||||
this.isInsertingNodes = false;
|
||||
|
||||
// Now it can draw again
|
||||
this.instance?.setSuspendDrawing(false, true);
|
||||
},
|
||||
async addConnections(connections: IConnections) {
|
||||
const batchedConnectionData: Array<[IConnection, IConnection]> = [];
|
||||
|
||||
for (const sourceNode in connections) {
|
||||
for (const type in connections[sourceNode]) {
|
||||
connections[sourceNode][type].forEach((outwardConnections, sourceIndex) => {
|
||||
if (outwardConnections) {
|
||||
outwardConnections.forEach((targetData) => {
|
||||
batchedConnectionData.push([
|
||||
{
|
||||
node: sourceNode,
|
||||
type: getEndpointScope(type) ?? NodeConnectionType.Main,
|
||||
index: sourceIndex,
|
||||
},
|
||||
{ node: targetData.node, type: targetData.type, index: targetData.index },
|
||||
]);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process the connections in batches
|
||||
await this.processConnectionBatch(batchedConnectionData);
|
||||
setTimeout(this.addConectionsTestData, 0);
|
||||
},
|
||||
|
||||
async processConnectionBatch(batchedConnectionData: Array<[IConnection, IConnection]>) {
|
||||
const batchSize = 100;
|
||||
|
||||
for (let i = 0; i < batchedConnectionData.length; i += batchSize) {
|
||||
const batch = batchedConnectionData.slice(i, i + batchSize);
|
||||
|
||||
batch.forEach((connectionData) => {
|
||||
this.__addConnection(connectionData);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async addNodesToWorkflow(data: IWorkflowDataUpdate): Promise<IWorkflowDataUpdate> {
|
||||
// Because nodes with the same name maybe already exist, it could
|
||||
// be needed that they have to be renamed. Also could it be possible
|
||||
@@ -4544,7 +4215,7 @@ export default defineComponent({
|
||||
let newName: string;
|
||||
const createNodes: INode[] = [];
|
||||
|
||||
await this.loadNodesProperties(
|
||||
await this.nodeHelpers.loadNodesProperties(
|
||||
data.nodes.map((node) => ({ name: node.type, version: node.typeVersion })),
|
||||
);
|
||||
|
||||
@@ -4660,7 +4331,7 @@ export default defineComponent({
|
||||
|
||||
// Add the nodes with the changed node names, expressions and connections
|
||||
this.historyStore.startRecordingUndo();
|
||||
await this.addNodes(
|
||||
await this.nodeHelpers.addNodes(
|
||||
Object.values(tempWorkflow.nodes),
|
||||
tempWorkflow.connectionsBySourceNode,
|
||||
true,
|
||||
@@ -4784,7 +4455,7 @@ export default defineComponent({
|
||||
this.uiStore.resetSelectedNodes();
|
||||
this.uiStore.nodeViewOffsetPosition = [0, 0];
|
||||
|
||||
this.credentialsUpdated = false;
|
||||
this.nodeHelpers.credentialsUpdated.value = false;
|
||||
},
|
||||
async loadActiveWorkflows(): Promise<void> {
|
||||
await this.workflowsStore.fetchActiveWorkflows();
|
||||
@@ -4824,30 +4495,6 @@ export default defineComponent({
|
||||
async loadSecrets(): Promise<void> {
|
||||
await this.externalSecretsStore.fetchAllSecrets();
|
||||
},
|
||||
async loadNodesProperties(nodeInfos: INodeTypeNameVersion[]): Promise<void> {
|
||||
const allNodes: INodeTypeDescription[] = this.nodeTypesStore.allNodeTypes;
|
||||
|
||||
const nodesToBeFetched: INodeTypeNameVersion[] = [];
|
||||
allNodes.forEach((node) => {
|
||||
const nodeVersions = Array.isArray(node.version) ? node.version : [node.version];
|
||||
if (
|
||||
!!nodeInfos.find((n) => n.name === node.name && nodeVersions.includes(n.version)) &&
|
||||
!node.hasOwnProperty('properties')
|
||||
) {
|
||||
nodesToBeFetched.push({
|
||||
name: node.name,
|
||||
version: Array.isArray(node.version) ? node.version.slice(-1)[0] : node.version,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (nodesToBeFetched.length > 0) {
|
||||
// Only call API if node information is actually missing
|
||||
this.canvasStore.startLoading();
|
||||
await this.nodeTypesStore.getNodesInformation(nodesToBeFetched);
|
||||
this.canvasStore.stopLoading();
|
||||
}
|
||||
},
|
||||
async onPostMessageReceived(message: MessageEvent) {
|
||||
if (!message || typeof message.data !== 'string' || !message.data?.includes?.('"command"')) {
|
||||
return;
|
||||
@@ -4880,7 +4527,7 @@ export default defineComponent({
|
||||
try {
|
||||
// If this NodeView is used in preview mode (in iframe) it will not have access to the main app store
|
||||
// so everything it needs has to be sent using post messages and passed down to child components
|
||||
this.isProductionExecutionPreview = json.executionMode !== 'manual';
|
||||
this.nodeHelpers.isProductionExecutionPreview.value = json.executionMode !== 'manual';
|
||||
|
||||
await this.openExecution(json.executionId);
|
||||
this.canOpenNDV = json.canOpenNDV ?? true;
|
||||
@@ -4918,73 +4565,6 @@ export default defineComponent({
|
||||
await this.importWorkflowData(workflowData, 'url');
|
||||
}
|
||||
},
|
||||
addPinDataConnections(pinData?: IPinData) {
|
||||
if (!pinData) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(pinData).forEach((nodeName) => {
|
||||
const node = this.workflowsStore.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeElement = document.getElementById(node.id);
|
||||
if (!nodeElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasRun = this.workflowsStore.getWorkflowResultDataByNodeName(nodeName) !== null;
|
||||
// In case we are showing a production execution preview we want
|
||||
// to show pinned data connections as they wouldn't have been pinned
|
||||
const classNames = this.isProductionExecutionPreview ? [] : ['pinned'];
|
||||
|
||||
if (hasRun) {
|
||||
classNames.push('has-run');
|
||||
}
|
||||
|
||||
const connections = this.instance?.getConnections({
|
||||
source: nodeElement,
|
||||
});
|
||||
|
||||
const connectionsArray = Array.isArray(connections)
|
||||
? connections
|
||||
: Object.values(connections);
|
||||
|
||||
connectionsArray.forEach((connection) => {
|
||||
NodeViewUtils.addConnectionOutputSuccess(connection, {
|
||||
total: pinData[nodeName].length,
|
||||
iterations: 0,
|
||||
classNames,
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
removePinDataConnections(pinData: IPinData) {
|
||||
Object.keys(pinData).forEach((nodeName) => {
|
||||
const node = this.workflowsStore.getNodeByName(nodeName);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nodeElement = document.getElementById(node.id);
|
||||
if (!nodeElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connections = this.instance?.getConnections({
|
||||
source: nodeElement,
|
||||
});
|
||||
|
||||
const connectionsArray = Array.isArray(connections)
|
||||
? connections
|
||||
: Object.values(connections);
|
||||
|
||||
this.instance.setSuspendDrawing(true);
|
||||
connectionsArray.forEach(NodeViewUtils.resetConnection);
|
||||
this.instance.setSuspendDrawing(false, true);
|
||||
});
|
||||
},
|
||||
onToggleNodeCreator({ source, createNodeActive, nodeCreatorView }: ToggleNodeCreatorOptions) {
|
||||
if (createNodeActive === this.createNodeActive) return;
|
||||
|
||||
@@ -5077,7 +4657,7 @@ export default defineComponent({
|
||||
});
|
||||
}
|
||||
|
||||
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
|
||||
this.nodeHelpers.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
|
||||
},
|
||||
|
||||
async saveCurrentWorkflowExternal(callback: () => void) {
|
||||
@@ -5108,16 +4688,16 @@ export default defineComponent({
|
||||
// For some reason, returning node to canvas with old id
|
||||
// makes it's endpoint to render at wrong position
|
||||
node.id = uuid();
|
||||
await this.addNodes([node]);
|
||||
await this.nodeHelpers.addNodes([node]);
|
||||
},
|
||||
onRevertAddConnection({ connection }: { connection: [IConnection, IConnection] }) {
|
||||
this.suspendRecordingDetachedConnections = true;
|
||||
this.__removeConnection(connection, true);
|
||||
this.nodeHelpers.removeConnection(connection, true);
|
||||
this.suspendRecordingDetachedConnections = false;
|
||||
},
|
||||
async onRevertRemoveConnection({ connection }: { connection: [IConnection, IConnection] }) {
|
||||
this.suspendRecordingDetachedConnections = true;
|
||||
this.__addConnection(connection);
|
||||
this.nodeHelpers.addConnection(connection);
|
||||
this.suspendRecordingDetachedConnections = false;
|
||||
},
|
||||
async onRevertNameChange({ currentName, newName }: { currentName: string; newName: string }) {
|
||||
|
||||
@@ -3,22 +3,22 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import WorkflowExecutionsList from '@/components/executions/workflow/WorkflowExecutionsList.vue';
|
||||
import { useExecutionsStore } from '@/stores/executions.store';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { ExecutionFilterType, ITag, IWorkflowDb } from '@/Interface';
|
||||
import type { ExecutionFilterType, IWorkflowDb } from '@/Interface';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { NO_NETWORK_ERROR_CODE } from '@/utils/apiUtils';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { VIEWS } from '@/constants';
|
||||
import { PLACEHOLDER_EMPTY_WORKFLOW_ID, VIEWS } from '@/constants';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import type { ExecutionSummary } from 'n8n-workflow';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useTagsStore } from '@/stores/tags.store';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
|
||||
const executionsStore = useExecutionsStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const tagsStore = useTagsStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
@@ -26,6 +26,8 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
const { callDebounced } = useDebounce();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
|
||||
const { filters } = storeToRefs(executionsStore);
|
||||
|
||||
@@ -117,29 +119,19 @@ async function initializeRoute() {
|
||||
}
|
||||
|
||||
async function fetchWorkflow() {
|
||||
let data: IWorkflowDb | undefined = workflowsStore.workflowsById[workflowId.value];
|
||||
if (!data) {
|
||||
// Check if the workflow already has an ID
|
||||
// In other words: are we coming from the Editor tab or browser loaded the Executions tab directly
|
||||
if (workflowsStore.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
|
||||
try {
|
||||
data = await workflowsStore.fetchWorkflow(workflowId.value);
|
||||
await workflowsStore.fetchActiveWorkflows();
|
||||
const data = await workflowsStore.fetchWorkflow(workflowId.value);
|
||||
await workflowHelpers.initState(data);
|
||||
await nodeHelpers.addNodes(data.nodes, data.connections);
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('nodeView.showError.openWorkflow.title'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
throw new Error(
|
||||
i18n.baseText('nodeView.workflowWithIdCouldNotBeFound', {
|
||||
interpolate: { workflowId: workflowId.value },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const tags = (data.tags ?? []) as ITag[];
|
||||
workflow.value = data;
|
||||
workflowsStore.setWorkflowName({ newName: data.name, setStateDirty: false });
|
||||
workflowsStore.setWorkflowTagIds(tags.map(({ id }) => id) ?? []);
|
||||
tagsStore.upsertTags(tags);
|
||||
workflow.value = workflowsStore.workflow;
|
||||
}
|
||||
|
||||
async function onAutoRefreshToggle(value: boolean) {
|
||||
|
||||
Reference in New Issue
Block a user