fix(editor): Properly update workflow info in main header (#9789)

This commit is contained in:
Csaba Tuncsik
2024-06-24 15:08:24 +02:00
committed by GitHub
parent 44ea4c73eb
commit 1ba656ef4a
9 changed files with 789 additions and 710 deletions

View File

@@ -2,6 +2,7 @@ import type { RouteHandler } from 'cypress/types/net-stubbing';
import { WorkflowPage } from '../pages'; import { WorkflowPage } from '../pages';
import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab'; import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab';
import executionOutOfMemoryServerResponse from '../fixtures/responses/execution-out-of-memory-server-response.json'; import executionOutOfMemoryServerResponse from '../fixtures/responses/execution-out-of-memory-server-response.json';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const executionsTab = new WorkflowExecutionsTab(); const executionsTab = new WorkflowExecutionsTab();
@@ -136,15 +137,90 @@ describe('Current Workflow Executions', () => {
executionsTab.getters.executionListItems().eq(14).should('not.be.visible'); 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(); executionsTab.actions.switchToExecutionsTab();
checkMainHeaderELements(); 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(); cy.reload();
checkMainHeaderELements(); 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(); executionsTab.actions.switchToEditorTab();
checkMainHeaderELements(); 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);
}); });
}); });

View File

@@ -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> <template>
<div> <div>
<div :class="{ 'main-header': true, expanded: !uiStore.sidebarMenuCollapsed }"> <div :class="{ 'main-header': true, expanded: !uiStore.sidebarMenuCollapsed }">
@@ -14,194 +172,6 @@
</div> </div>
</template> </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"> <style lang="scss">
.main-header { .main-header {
background-color: var(--color-background-xlight); background-color: var(--color-background-xlight);

View File

@@ -110,7 +110,7 @@ export default defineComponent({
default: () => ({}), default: () => ({}),
}, },
execution: { execution: {
type: Object as PropType<ExecutionSummary>, type: Object as PropType<ExecutionSummary> | null,
default: null, default: null,
}, },
loadingMore: { loadingMore: {
@@ -179,10 +179,10 @@ export default defineComponent({
}, },
methods: { methods: {
async onDeleteCurrentExecution(): Promise<void> { async onDeleteCurrentExecution(): Promise<void> {
this.$emit('execution:delete', this.execution.id); this.$emit('execution:delete', this.execution?.id);
}, },
async onStopExecution(): Promise<void> { 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 }) { async onRetryExecution(payload: { execution: ExecutionSummary; command: string }) {
const loadWorkflow = payload.command === 'current-workflow'; const loadWorkflow = payload.command === 'current-workflow';

View File

@@ -143,6 +143,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { ElDropdown } from 'element-plus'; import { ElDropdown } from 'element-plus';
import { useExecutionDebugging } from '@/composables/useExecutionDebugging'; import { useExecutionDebugging } from '@/composables/useExecutionDebugging';
@@ -165,7 +166,7 @@ export default defineComponent({
}, },
props: { props: {
execution: { execution: {
type: Object as () => ExecutionSummary | null, type: Object as PropType<ExecutionSummary>,
required: true, required: true,
}, },
}, },

View File

@@ -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 { useHistoryStore } from '@/stores/history.store';
import { import {
CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_KEY,
FORM_TRIGGER_NODE_TYPE,
NODE_OUTPUT_DEFAULT_KEY, NODE_OUTPUT_DEFAULT_KEY,
PLACEHOLDER_FILLED_AT_EXECUTION_TIME, PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
WEBHOOK_NODE_TYPE,
} from '@/constants'; } from '@/constants';
import { NodeHelpers, NodeConnectionType, ExpressionEvaluatorProxy } from 'n8n-workflow'; import { NodeHelpers, NodeConnectionType, ExpressionEvaluatorProxy } from 'n8n-workflow';
@@ -26,6 +32,10 @@ import type {
INodeCredentialsDetails, INodeCredentialsDetails,
INodeParameters, INodeParameters,
ITaskData, ITaskData,
IConnections,
INodeTypeNameVersion,
IConnection,
IPinData,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { import type {
@@ -43,12 +53,15 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useCredentialsStore } from '@/stores/credentials.store'; import { useCredentialsStore } from '@/stores/credentials.store';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
import { useI18n } from './useI18n'; import { useI18n } from './useI18n';
import { EnableNodeToggleCommand } from '@/models/history'; import { AddNodeCommand, EnableNodeToggleCommand, RemoveConnectionCommand } from '@/models/history';
import { useTelemetry } from './useTelemetry'; import { useTelemetry } from './useTelemetry';
import { hasPermission } from '@/utils/rbac/permissions'; import { hasPermission } from '@/utils/rbac/permissions';
import type { N8nPlusEndpoint } from '@/plugins/jsplumb/N8nPlusEndpointType'; import type { N8nPlusEndpoint } from '@/plugins/jsplumb/N8nPlusEndpointType';
import * as NodeViewUtils from '@/utils/nodeViewUtils'; import * as NodeViewUtils from '@/utils/nodeViewUtils';
import { useCanvasStore } from '@/stores/canvas.store'; 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 { declare namespace HttpRequestNode {
namespace V2 { namespace V2 {
@@ -67,6 +80,13 @@ export function useNodeHelpers() {
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const i18n = useI18n(); const i18n = useI18n();
const canvasStore = useCanvasStore(); 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 { function hasProxyAuth(node: INodeUi): boolean {
return Object.keys(node.parameters).includes('nodeCredentialType'); 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 { return {
hasProxyAuth, hasProxyAuth,
isCustomApiCallSelected, isCustomApiCallSelected,
@@ -795,5 +1239,17 @@ export function useNodeHelpers() {
updateNodesCredentialsIssues, updateNodesCredentialsIssues,
getNodeInputData, getNodeInputData,
setSuccessOutput, setSuccessOutput,
isInsertingNodes,
credentialsUpdated,
isProductionExecutionPreview,
pullConnActiveNodeName,
deleteJSPlumbConnection,
loadNodesProperties,
addNodes,
addConnection,
removeConnection,
removeConnectionByConnectionInfo,
addPinDataConnections,
removePinDataConnections,
}; };
} }

View File

@@ -68,6 +68,7 @@ import { useI18n } from '@/composables/useI18n';
import type { useRouter } from 'vue-router'; import type { useRouter } from 'vue-router';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { useProjectsStore } from '@/stores/projects.store'; import { useProjectsStore } from '@/stores/projects.store';
import { useTagsStore } from '@/stores/tags.store';
export function resolveParameter<T = IDataObject>( export function resolveParameter<T = IDataObject>(
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
@@ -483,6 +484,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
const uiStore = useUIStore(); const uiStore = useUIStore();
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const projectsStore = useProjectsStore(); const projectsStore = useProjectsStore();
const tagsStore = useTagsStore();
const toast = useToast(); const toast = useToast();
const message = useMessage(); 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 { return {
resolveParameter, resolveParameter,
resolveRequiredParameters, resolveRequiredParameters,
@@ -1214,5 +1235,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
dataHasChanged, dataHasChanged,
removeForeignCredentialsFromWorkflow, removeForeignCredentialsFromWorkflow,
getWorkflowProjectRole, getWorkflowProjectRole,
initState,
}; };
} }

View File

@@ -73,8 +73,6 @@ import { i18n } from '@/plugins/i18n';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useProjectsStore } from '@/stores/projects.store'; 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']> } = { const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
name: '', name: '',
@@ -101,6 +99,8 @@ let cachedWorkflowKey: string | null = '';
let cachedWorkflow: Workflow | null = null; let cachedWorkflow: Workflow | null = null;
export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const uiStore = useUIStore();
const workflow = ref<IWorkflowDb>(createEmptyWorkflow()); const workflow = ref<IWorkflowDb>(createEmptyWorkflow());
const usedCredentials = ref<Record<string, IUsedCredential>>({}); const usedCredentials = ref<Record<string, IUsedCredential>>({});
@@ -436,8 +436,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
function resetWorkflow() { function resetWorkflow() {
const usersStore = useUsersStore();
const settingsStore = useSettingsStore();
workflow.value = createEmptyWorkflow(); workflow.value = createEmptyWorkflow();
} }
@@ -481,7 +479,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
function setWorkflowName(data: { newName: string; setStateDirty: boolean }) { function setWorkflowName(data: { newName: string; setStateDirty: boolean }) {
if (data.setStateDirty) { if (data.setStateDirty) {
const uiStore = useUIStore();
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
} }
workflow.value.name = data.newName; workflow.value.name = data.newName;
@@ -560,7 +557,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
function setWorkflowActive(targetWorkflowId: string) { function setWorkflowActive(targetWorkflowId: string) {
const uiStore = useUIStore();
uiStore.stateIsDirty = false; uiStore.stateIsDirty = false;
const index = activeWorkflows.value.indexOf(targetWorkflowId); const index = activeWorkflows.value.indexOf(targetWorkflowId);
if (index === -1) { if (index === -1) {
@@ -648,10 +644,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
function setWorkflowTagIds(tags: string[]) { function setWorkflowTagIds(tags: string[]) {
workflow.value = { workflow.value.tags = tags;
...workflow.value,
tags,
};
} }
function addWorkflowTagIds(tags: string[]) { function addWorkflowTagIds(tags: string[]) {
@@ -715,7 +708,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
}, },
}; };
const uiStore = useUIStore();
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
dataPinningEventBus.emit('pin-data', { [payload.node.name]: storedPinData }); dataPinningEventBus.emit('pin-data', { [payload.node.name]: storedPinData });
@@ -732,7 +724,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
pinData, pinData,
}; };
const uiStore = useUIStore();
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
dataPinningEventBus.emit('unpin-data', { [payload.node.name]: undefined }); dataPinningEventBus.emit('unpin-data', { [payload.node.name]: undefined });
@@ -829,7 +820,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
return; return;
} }
const uiStore = useUIStore();
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
const connections = const connections =
@@ -847,8 +837,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
function removeAllConnections(data: { setStateDirty: boolean }): void { function removeAllConnections(data: { setStateDirty: boolean }): void {
if (data && data.setStateDirty) { if (data?.setStateDirty) {
const uiStore = useUIStore();
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
} }
@@ -859,7 +848,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
node: INodeUi, node: INodeUi,
{ preserveInputConnections = false, preserveOutputConnections = false } = {}, { preserveInputConnections = false, preserveOutputConnections = false } = {},
): void { ): void {
const uiStore = useUIStore();
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
// Remove all source connections // Remove all source connections
@@ -904,7 +892,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
function renameNodeSelectedAndExecution(nameData: { old: string; new: string }): void { function renameNodeSelectedAndExecution(nameData: { old: string; new: string }): void {
const uiStore = useUIStore();
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
// If node has any WorkflowResultData rename also that one that the data // 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 { function removeNode(node: INodeUi): void {
const uiStore = useUIStore();
const { [node.name]: removedNodeMetadata, ...remainingNodeMetadata } = nodeMetadata.value; const { [node.name]: removedNodeMetadata, ...remainingNodeMetadata } = nodeMetadata.value;
nodeMetadata.value = remainingNodeMetadata; nodeMetadata.value = remainingNodeMetadata;
@@ -1051,7 +1037,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
function removeAllNodes(data: { setStateDirty: boolean; removePinData: boolean }): void { function removeAllNodes(data: { setStateDirty: boolean; removePinData: boolean }): void {
if (data.setStateDirty) { if (data.setStateDirty) {
const uiStore = useUIStore();
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
} }
@@ -1074,7 +1059,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
if (nodeIndex !== -1) { if (nodeIndex !== -1) {
for (const key of Object.keys(updateInformation.properties)) { for (const key of Object.keys(updateInformation.properties)) {
const uiStore = useUIStore();
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
updateNodeAtIndex(nodeIndex, { updateNodeAtIndex(nodeIndex, {
@@ -1096,7 +1080,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
); );
} }
const uiStore = useUIStore();
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
updateNodeAtIndex(nodeIndex, { updateNodeAtIndex(nodeIndex, {
@@ -1118,7 +1101,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const node = workflow.value.nodes[nodeIndex]; const node = workflow.value.nodes[nodeIndex];
const uiStore = useUIStore();
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
const newParameters = const newParameters =
!!append && isObject(updateInformation.value) !!append && isObject(updateInformation.value)

View File

@@ -235,8 +235,6 @@ import {
START_NODE_TYPE, START_NODE_TYPE,
STICKY_NODE_TYPE, STICKY_NODE_TYPE,
VIEWS, VIEWS,
WEBHOOK_NODE_TYPE,
FORM_TRIGGER_NODE_TYPE,
TRIGGER_NODE_CREATOR_VIEW, TRIGGER_NODE_CREATOR_VIEW,
EnterpriseEditionFeature, EnterpriseEditionFeature,
REGULAR_NODE_CREATOR_VIEW, REGULAR_NODE_CREATOR_VIEW,
@@ -279,11 +277,8 @@ import type {
ExecutionSummary, ExecutionSummary,
INode, INode,
INodeConnections, INodeConnections,
INodeCredentialsDetails,
INodeInputConfiguration, INodeInputConfiguration,
INodeTypeDescription, INodeTypeDescription,
INodeTypeNameVersion,
IPinData,
ITaskData, ITaskData,
ITelemetryTrackProperties, ITelemetryTrackProperties,
IWorkflowBase, IWorkflowBase,
@@ -302,7 +297,6 @@ import {
} from 'n8n-workflow'; } from 'n8n-workflow';
import type { import type {
NewConnectionInfo, NewConnectionInfo,
ICredentialsResponse,
IExecutionResponse, IExecutionResponse,
IWorkflowDb, IWorkflowDb,
IWorkflowData, IWorkflowData,
@@ -350,7 +344,6 @@ import { getAccountAge } from '@/utils/userUtils';
import { getConnectionInfo, getNodeViewTab } from '@/utils/canvasUtils'; import { getConnectionInfo, getNodeViewTab } from '@/utils/canvasUtils';
import { import {
AddConnectionCommand, AddConnectionCommand,
AddNodeCommand,
MoveNodeCommand, MoveNodeCommand,
RemoveConnectionCommand, RemoveConnectionCommand,
RemoveNodeCommand, RemoveNodeCommand,
@@ -550,8 +543,6 @@ export default defineComponent({
moveCanvasKeyPressed: false, moveCanvasKeyPressed: false,
stopExecutionInProgress: false, stopExecutionInProgress: false,
blankRedirect: false, blankRedirect: false,
credentialsUpdated: false,
pullConnActiveNodeName: null as string | null,
pullConnActive: false, pullConnActive: false,
dropPrevented: false, dropPrevented: false,
connectionDragScope: { connectionDragScope: {
@@ -564,8 +555,6 @@ export default defineComponent({
showTriggerMissingTooltip: false, showTriggerMissingTooltip: false,
workflowData: null as INewWorkflowData | null, workflowData: null as INewWorkflowData | null,
activeConnection: null as null | Connection, activeConnection: null as null | Connection,
isInsertingNodes: false,
isProductionExecutionPreview: false,
enterTimer: undefined as undefined | ReturnType<typeof setTimeout>, enterTimer: undefined as undefined | ReturnType<typeof setTimeout>,
exitTimer: undefined as undefined | ReturnType<typeof setTimeout>, exitTimer: undefined as undefined | ReturnType<typeof setTimeout>,
readOnlyNotification: null as null | NotificationHandle, readOnlyNotification: null as null | NotificationHandle,
@@ -779,6 +768,9 @@ export default defineComponent({
const isCloudDeployment = this.settingsStore.isCloudDeployment; const isCloudDeployment = this.settingsStore.isCloudDeployment;
return isCloudDeployment && experimentEnabled && !userHasSeenAIAssistantExperiment; return isCloudDeployment && experimentEnabled && !userHasSeenAIAssistantExperiment;
}, },
isProductionExecutionPreview(): boolean {
return this.nodeHelpers.isProductionExecutionPreview.value;
},
}, },
watch: { watch: {
// Listen to route changes and load the workflow accordingly // Listen to route changes and load the workflow accordingly
@@ -916,7 +908,7 @@ export default defineComponent({
setTimeout(() => { setTimeout(() => {
void this.usersStore.showPersonalizationSurvey(); void this.usersStore.showPersonalizationSurvey();
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData); this.nodeHelpers.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
}, 0); }, 0);
}); });
@@ -1007,8 +999,8 @@ export default defineComponent({
historyBus.on('revertRenameNode', this.onRevertNameChange); historyBus.on('revertRenameNode', this.onRevertNameChange);
historyBus.on('enableNodeToggle', this.onRevertEnableToggle); historyBus.on('enableNodeToggle', this.onRevertEnableToggle);
dataPinningEventBus.on('pin-data', this.addPinDataConnections); dataPinningEventBus.on('pin-data', this.nodeHelpers.addPinDataConnections);
dataPinningEventBus.on('unpin-data', this.removePinDataConnections); dataPinningEventBus.on('unpin-data', this.nodeHelpers.removePinDataConnections);
nodeViewEventBus.on('saveWorkflow', this.saveCurrentWorkflowExternal); nodeViewEventBus.on('saveWorkflow', this.saveCurrentWorkflowExternal);
this.canvasStore.isDemo = this.isDemo; this.canvasStore.isDemo = this.isDemo;
@@ -1033,8 +1025,8 @@ export default defineComponent({
historyBus.off('revertRenameNode', this.onRevertNameChange); historyBus.off('revertRenameNode', this.onRevertNameChange);
historyBus.off('enableNodeToggle', this.onRevertEnableToggle); historyBus.off('enableNodeToggle', this.onRevertEnableToggle);
dataPinningEventBus.off('pin-data', this.addPinDataConnections); dataPinningEventBus.off('pin-data', this.nodeHelpers.addPinDataConnections);
dataPinningEventBus.off('unpin-data', this.removePinDataConnections); dataPinningEventBus.off('unpin-data', this.nodeHelpers.removePinDataConnections);
nodeViewEventBus.off('saveWorkflow', this.saveCurrentWorkflowExternal); nodeViewEventBus.off('saveWorkflow', this.saveCurrentWorkflowExternal);
}, },
beforeMount() { beforeMount() {
@@ -1334,7 +1326,7 @@ export default defineComponent({
this.workflowsStore.setUsedCredentials(data.workflowData.usedCredentials); this.workflowsStore.setUsedCredentials(data.workflowData.usedCredentials);
} }
await this.addNodes( await this.nodeHelpers.addNodes(
deepCopy(data.workflowData.nodes), deepCopy(data.workflowData.nodes),
deepCopy(data.workflowData.connections), deepCopy(data.workflowData.connections),
); );
@@ -1405,7 +1397,7 @@ export default defineComponent({
this.resetWorkspace(); this.resetWorkspace();
data.workflow.nodes = NodeViewUtils.getFixedNodesList(data.workflow.nodes); 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) { if (data.workflow.pinData) {
this.workflowsStore.setWorkflowPinData(data.workflow.pinData); this.workflowsStore.setWorkflowPinData(data.workflow.pinData);
@@ -1457,7 +1449,7 @@ export default defineComponent({
const convertedNodes = data.workflow.nodes.map( const convertedNodes = data.workflow.nodes.map(
this.workflowsStore.convertTemplateNodeToNodeUi, this.workflowsStore.convertTemplateNodeToNodeUi,
); );
await this.addNodes(convertedNodes, data.workflow.connections); await this.nodeHelpers.addNodes(convertedNodes, data.workflow.connections);
this.workflowData = this.workflowData =
(await this.workflowsStore.getNewWorkflowData( (await this.workflowsStore.getNewWorkflowData(
data.name, data.name,
@@ -1482,14 +1474,7 @@ export default defineComponent({
this.resetWorkspace(); this.resetWorkspace();
this.workflowsStore.addWorkflow(workflow); await this.workflowHelpers.initState(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);
if (workflow.sharedWithProjects) { if (workflow.sharedWithProjects) {
this.workflowsEEStore.setWorkflowSharedWith({ this.workflowsEEStore.setWorkflowSharedWith({
@@ -1502,14 +1487,9 @@ export default defineComponent({
this.workflowsStore.setUsedCredentials(workflow.usedCredentials); this.workflowsStore.setUsedCredentials(workflow.usedCredentials);
} }
const tags = (workflow.tags ?? []) as ITag[]; await this.nodeHelpers.addNodes(workflow.nodes, workflow.connections);
const tagIds = tags.map((tag) => tag.id);
this.workflowsStore.setWorkflowTagIds(tagIds || []);
this.tagsStore.upsertTags(tags);
await this.addNodes(workflow.nodes, workflow.connections); if (!this.nodeHelpers.credentialsUpdated.value) {
if (!this.credentialsUpdated) {
this.uiStore.stateIsDirty = false; this.uiStore.stateIsDirty = false;
} }
this.canvasStore.zoomToFit(); this.canvasStore.zoomToFit();
@@ -2340,7 +2320,7 @@ export default defineComponent({
this.workflowsStore.addWorkflowTagIds(tagIds); this.workflowsStore.addWorkflowTagIds(tagIds);
setTimeout(() => { setTimeout(() => {
this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData); this.nodeHelpers.addPinDataConnections(this.workflowsStore.pinnedWorkflowData);
}); });
} }
} catch (error) { } catch (error) {
@@ -2461,7 +2441,7 @@ export default defineComponent({
[defaultCredential.type]: selected, [defaultCredential.type]: selected,
}; };
await this.loadNodesProperties( await this.nodeHelpers.loadNodesProperties(
[newNodeData].map((node) => ({ name: node.type, version: node.typeVersion })), [newNodeData].map((node) => ({ name: node.type, version: node.typeVersion })),
); );
const nodeType = this.nodeTypesStore.getNodeType(newNodeData.type, newNodeData.typeVersion); const nodeType = this.nodeTypesStore.getNodeType(newNodeData.type, newNodeData.typeVersion);
@@ -2703,7 +2683,7 @@ export default defineComponent({
newNodeData.webhookId = uuid(); newNodeData.webhookId = uuid();
} }
await this.addNodes([newNodeData], undefined, trackHistory); await this.nodeHelpers.addNodes([newNodeData], undefined, trackHistory);
this.workflowsStore.setNodePristine(newNodeData.name, true); this.workflowsStore.setNodePristine(newNodeData.name, true);
this.uiStore.stateIsDirty = true; this.uiStore.stateIsDirty = true;
@@ -2811,7 +2791,7 @@ export default defineComponent({
}, },
] as [IConnection, IConnection]; ] as [IConnection, IConnection];
this.__addConnection(connectionData); this.nodeHelpers.addConnection(connectionData);
}, },
async addNode( async addNode(
nodeTypeName: string, nodeTypeName: string,
@@ -2879,7 +2859,7 @@ export default defineComponent({
await this.$nextTick(); await this.$nextTick();
if (lastSelectedConnection?.__meta) { if (lastSelectedConnection?.__meta) {
this.__deleteJSPlumbConnection(lastSelectedConnection, trackHistory); this.nodeHelpers.deleteJSPlumbConnection(lastSelectedConnection, trackHistory);
const targetNodeName = lastSelectedConnection.__meta.targetNodeName; const targetNodeName = lastSelectedConnection.__meta.targetNodeName;
const targetOutputIndex = lastSelectedConnection.__meta.targetOutputIndex; const targetOutputIndex = lastSelectedConnection.__meta.targetOutputIndex;
@@ -2980,7 +2960,7 @@ export default defineComponent({
this.dropPrevented = false; this.dropPrevented = false;
return; return;
} }
if (this.pullConnActiveNodeName) { if (this.nodeHelpers.pullConnActiveNodeName.value) {
const sourceNode = this.workflowsStore.getNodeById(connection.parameters.nodeId); const sourceNode = this.workflowsStore.getNodeById(connection.parameters.nodeId);
const connectionType = connection.parameters.type ?? NodeConnectionType.Main; const connectionType = connection.parameters.type ?? NodeConnectionType.Main;
const overrideTargetEndpoint = connection?.connector const overrideTargetEndpoint = connection?.connector
@@ -2988,8 +2968,12 @@ export default defineComponent({
if (sourceNode) { if (sourceNode) {
const isTarget = connection.parameters.connection === 'target'; const isTarget = connection.parameters.connection === 'target';
const sourceNodeName = isTarget ? this.pullConnActiveNodeName : sourceNode.name; const sourceNodeName = isTarget
const targetNodeName = isTarget ? sourceNode.name : this.pullConnActiveNodeName; ? this.nodeHelpers.pullConnActiveNodeName.value
: sourceNode.name;
const targetNodeName = isTarget
? sourceNode.name
: this.nodeHelpers.pullConnActiveNodeName.value;
const outputIndex = connection.parameters.index; const outputIndex = connection.parameters.index;
NodeViewUtils.resetConnectionAfterPull(connection); NodeViewUtils.resetConnectionAfterPull(connection);
await this.$nextTick(); await this.$nextTick();
@@ -3001,7 +2985,7 @@ export default defineComponent({
overrideTargetEndpoint?.parameters?.index ?? 0, overrideTargetEndpoint?.parameters?.index ?? 0,
connectionType, connectionType,
); );
this.pullConnActiveNodeName = null; this.nodeHelpers.pullConnActiveNodeName.value = null;
this.dropPrevented = false; this.dropPrevented = false;
} }
return; return;
@@ -3149,7 +3133,7 @@ export default defineComponent({
) )
) { ) {
this.dropPrevented = true; this.dropPrevented = true;
this.pullConnActiveNodeName = null; this.nodeHelpers.pullConnActiveNodeName.value = null;
return false; return false;
} }
@@ -3213,7 +3197,7 @@ export default defineComponent({
info.connection, info.connection,
() => { () => {
this.activeConnection = null; this.activeConnection = null;
this.__deleteJSPlumbConnection(info.connection); this.nodeHelpers.deleteJSPlumbConnection(info.connection);
}, },
() => { () => {
this.insertNodeAfterSelected({ 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. // 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 // 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.nodeHelpers.updateNodesInputIssues();
this.resetEndpointsErrors(); this.resetEndpointsErrors();
setTimeout(() => { setTimeout(() => {
@@ -3263,17 +3247,6 @@ export default defineComponent({
console.error(e); console.error(e);
} }
}, },
addConectionsTestData() {
this.instance.connections.forEach((connection) => {
NodeViewUtils.addConnectionTestData(
connection.source,
connection.target,
connection?.connector?.hasOwnProperty('canvas')
? connection?.connector.canvas
: undefined,
);
});
},
onDragMove() { onDragMove() {
const totalNodes = this.nodes.length; const totalNodes = this.nodes.length;
void this.callDebounced(this.updateConnectionsOverlays, { void this.callDebounced(this.updateConnectionsOverlays, {
@@ -3390,7 +3363,7 @@ export default defineComponent({
}, },
] as [IConnection, IConnection]; ] as [IConnection, IConnection];
this.__removeConnection(connectionInfo, false); this.nodeHelpers.removeConnection(connectionInfo, false);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
@@ -3420,9 +3393,9 @@ export default defineComponent({
NodeViewUtils.showOutputNameLabel(info.sourceEndpoint, info.connection); NodeViewUtils.showOutputNameLabel(info.sourceEndpoint, info.connection);
info.connection.removeOverlays(); 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 // establish new connection when dragging connection from one node to another
this.historyStore.startRecordingUndo(); this.historyStore.startRecordingUndo();
const sourceNode = this.workflowsStore.getNodeById(info.connection.parameters.nodeId); const sourceNode = this.workflowsStore.getNodeById(info.connection.parameters.nodeId);
@@ -3442,11 +3415,11 @@ export default defineComponent({
this.connectTwoNodes( this.connectTwoNodes(
sourceNodeName, sourceNodeName,
outputIndex, outputIndex,
this.pullConnActiveNodeName, this.nodeHelpers.pullConnActiveNodeName.value,
overrideTargetEndpoint?.parameters?.index ?? 0, overrideTargetEndpoint?.parameters?.index ?? 0,
NodeConnectionType.Main, NodeConnectionType.Main,
); );
this.pullConnActiveNodeName = null; this.nodeHelpers.pullConnActiveNodeName.value = null;
await this.$nextTick(); await this.$nextTick();
this.historyStore.stopRecordingUndo(); this.historyStore.stopRecordingUndo();
} else if ( } else if (
@@ -3470,7 +3443,7 @@ export default defineComponent({
// manually // manually
connection.overlays['midpoint-arrow']?.setVisible(false); connection.overlays['midpoint-arrow']?.setVisible(false);
try { try {
this.pullConnActiveNodeName = null; this.nodeHelpers.pullConnActiveNodeName.value = null;
this.pullConnActive = true; this.pullConnActive = true;
this.canvasStore.newNodeInsertPosition = null; this.canvasStore.newNodeInsertPosition = null;
NodeViewUtils.hideConnectionActions(connection); NodeViewUtils.hideConnectionActions(connection);
@@ -3555,12 +3528,12 @@ export default defineComponent({
const endpoint = intersectingEndpoint.jtk.endpoint as Endpoint; const endpoint = intersectingEndpoint.jtk.endpoint as Endpoint;
const node = this.workflowsStore.getNodeById(endpoint.parameters.nodeId); const node = this.workflowsStore.getNodeById(endpoint.parameters.nodeId);
this.pullConnActiveNodeName = node?.name ?? null; this.nodeHelpers.pullConnActiveNodeName.value = node?.name ?? null;
NodeViewUtils.showDropConnectionState(connection, endpoint); NodeViewUtils.showDropConnectionState(connection, endpoint);
} else { } else {
NodeViewUtils.showPullConnectionState(connection); NodeViewUtils.showPullConnectionState(connection);
this.pullConnActiveNodeName = null; this.nodeHelpers.pullConnActiveNodeName.value = null;
} }
}; };
@@ -3762,7 +3735,7 @@ export default defineComponent({
) { ) {
const manualTriggerNode = this.canvasStore.getAutoAddManualTriggerNode(); const manualTriggerNode = this.canvasStore.getAutoAddManualTriggerNode();
if (manualTriggerNode) { if (manualTriggerNode) {
await this.addNodes([manualTriggerNode]); await this.nodeHelpers.addNodes([manualTriggerNode]);
this.uiStore.lastSelectedNode = manualTriggerNode.name; this.uiStore.lastSelectedNode = manualTriggerNode.name;
} }
} }
@@ -3857,130 +3830,6 @@ export default defineComponent({
// waiting in the store and display them // waiting in the store and display them
this.showNotificationForViews([VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW]); 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> { async duplicateNodes(nodes: INode[]): Promise<void> {
if (!this.editAllowedCheck()) { if (!this.editAllowedCheck()) {
return; return;
@@ -4310,7 +4159,11 @@ export default defineComponent({
await this.$nextTick(); await this.$nextTick();
// Add the new updated nodes // 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 // Make sure that the node is selected again
this.deselectAllNodes(); this.deselectAllNodes();
@@ -4337,188 +4190,6 @@ export default defineComponent({
this.instance.deleteEveryConnection({ fireEvent: true }); 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> { async addNodesToWorkflow(data: IWorkflowDataUpdate): Promise<IWorkflowDataUpdate> {
// Because nodes with the same name maybe already exist, it could // Because nodes with the same name maybe already exist, it could
// be needed that they have to be renamed. Also could it be possible // be needed that they have to be renamed. Also could it be possible
@@ -4544,7 +4215,7 @@ export default defineComponent({
let newName: string; let newName: string;
const createNodes: INode[] = []; const createNodes: INode[] = [];
await this.loadNodesProperties( await this.nodeHelpers.loadNodesProperties(
data.nodes.map((node) => ({ name: node.type, version: node.typeVersion })), 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 // Add the nodes with the changed node names, expressions and connections
this.historyStore.startRecordingUndo(); this.historyStore.startRecordingUndo();
await this.addNodes( await this.nodeHelpers.addNodes(
Object.values(tempWorkflow.nodes), Object.values(tempWorkflow.nodes),
tempWorkflow.connectionsBySourceNode, tempWorkflow.connectionsBySourceNode,
true, true,
@@ -4784,7 +4455,7 @@ export default defineComponent({
this.uiStore.resetSelectedNodes(); this.uiStore.resetSelectedNodes();
this.uiStore.nodeViewOffsetPosition = [0, 0]; this.uiStore.nodeViewOffsetPosition = [0, 0];
this.credentialsUpdated = false; this.nodeHelpers.credentialsUpdated.value = false;
}, },
async loadActiveWorkflows(): Promise<void> { async loadActiveWorkflows(): Promise<void> {
await this.workflowsStore.fetchActiveWorkflows(); await this.workflowsStore.fetchActiveWorkflows();
@@ -4824,30 +4495,6 @@ export default defineComponent({
async loadSecrets(): Promise<void> { async loadSecrets(): Promise<void> {
await this.externalSecretsStore.fetchAllSecrets(); 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) { async onPostMessageReceived(message: MessageEvent) {
if (!message || typeof message.data !== 'string' || !message.data?.includes?.('"command"')) { if (!message || typeof message.data !== 'string' || !message.data?.includes?.('"command"')) {
return; return;
@@ -4880,7 +4527,7 @@ export default defineComponent({
try { try {
// If this NodeView is used in preview mode (in iframe) it will not have access to the main app store // 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 // 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); await this.openExecution(json.executionId);
this.canOpenNDV = json.canOpenNDV ?? true; this.canOpenNDV = json.canOpenNDV ?? true;
@@ -4918,73 +4565,6 @@ export default defineComponent({
await this.importWorkflowData(workflowData, 'url'); 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) { onToggleNodeCreator({ source, createNodeActive, nodeCreatorView }: ToggleNodeCreatorOptions) {
if (createNodeActive === this.createNodeActive) return; 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) { async saveCurrentWorkflowExternal(callback: () => void) {
@@ -5108,16 +4688,16 @@ export default defineComponent({
// For some reason, returning node to canvas with old id // For some reason, returning node to canvas with old id
// makes it's endpoint to render at wrong position // makes it's endpoint to render at wrong position
node.id = uuid(); node.id = uuid();
await this.addNodes([node]); await this.nodeHelpers.addNodes([node]);
}, },
onRevertAddConnection({ connection }: { connection: [IConnection, IConnection] }) { onRevertAddConnection({ connection }: { connection: [IConnection, IConnection] }) {
this.suspendRecordingDetachedConnections = true; this.suspendRecordingDetachedConnections = true;
this.__removeConnection(connection, true); this.nodeHelpers.removeConnection(connection, true);
this.suspendRecordingDetachedConnections = false; this.suspendRecordingDetachedConnections = false;
}, },
async onRevertRemoveConnection({ connection }: { connection: [IConnection, IConnection] }) { async onRevertRemoveConnection({ connection }: { connection: [IConnection, IConnection] }) {
this.suspendRecordingDetachedConnections = true; this.suspendRecordingDetachedConnections = true;
this.__addConnection(connection); this.nodeHelpers.addConnection(connection);
this.suspendRecordingDetachedConnections = false; this.suspendRecordingDetachedConnections = false;
}, },
async onRevertNameChange({ currentName, newName }: { currentName: string; newName: string }) { async onRevertNameChange({ currentName, newName }: { currentName: string; newName: string }) {

View File

@@ -3,22 +3,22 @@ import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import WorkflowExecutionsList from '@/components/executions/workflow/WorkflowExecutionsList.vue'; import WorkflowExecutionsList from '@/components/executions/workflow/WorkflowExecutionsList.vue';
import { useExecutionsStore } from '@/stores/executions.store'; import { useExecutionsStore } from '@/stores/executions.store';
import { useI18n } from '@/composables/useI18n'; 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 { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { NO_NETWORK_ERROR_CODE } from '@/utils/apiUtils'; import { NO_NETWORK_ERROR_CODE } from '@/utils/apiUtils';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { VIEWS } from '@/constants'; import { PLACEHOLDER_EMPTY_WORKFLOW_ID, VIEWS } from '@/constants';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import type { ExecutionSummary } from 'n8n-workflow'; import type { ExecutionSummary } from 'n8n-workflow';
import { useDebounce } from '@/composables/useDebounce'; import { useDebounce } from '@/composables/useDebounce';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useTelemetry } from '@/composables/useTelemetry'; 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 executionsStore = useExecutionsStore();
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const tagsStore = useTagsStore();
const nodeTypesStore = useNodeTypesStore(); const nodeTypesStore = useNodeTypesStore();
const i18n = useI18n(); const i18n = useI18n();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
@@ -26,6 +26,8 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
const toast = useToast(); const toast = useToast();
const { callDebounced } = useDebounce(); const { callDebounced } = useDebounce();
const workflowHelpers = useWorkflowHelpers({ router });
const nodeHelpers = useNodeHelpers();
const { filters } = storeToRefs(executionsStore); const { filters } = storeToRefs(executionsStore);
@@ -117,29 +119,19 @@ async function initializeRoute() {
} }
async function fetchWorkflow() { async function fetchWorkflow() {
let data: IWorkflowDb | undefined = workflowsStore.workflowsById[workflowId.value]; // Check if the workflow already has an ID
if (!data) { // 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 { 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) { } catch (error) {
toast.showError(error, i18n.baseText('nodeView.showError.openWorkflow.title')); toast.showError(error, i18n.baseText('nodeView.showError.openWorkflow.title'));
return;
} }
} }
workflow.value = workflowsStore.workflow;
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);
} }
async function onAutoRefreshToggle(value: boolean) { async function onAutoRefreshToggle(value: boolean) {