mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(editor): Add ability to import workflows in new canvas (no-changelog) (#10051)
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
@@ -9,45 +9,45 @@ import type {
|
|||||||
ROLE,
|
ROLE,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import type { IMenuItem, NodeCreatorTag } from 'n8n-design-system';
|
import type { IMenuItem, NodeCreatorTag } from 'n8n-design-system';
|
||||||
import {
|
import type {
|
||||||
type GenericValue,
|
GenericValue,
|
||||||
type IConnections,
|
IConnections,
|
||||||
type ICredentialsDecrypted,
|
ICredentialsDecrypted,
|
||||||
type ICredentialsEncrypted,
|
ICredentialsEncrypted,
|
||||||
type ICredentialType,
|
ICredentialType,
|
||||||
type IDataObject,
|
IDataObject,
|
||||||
type INode,
|
INode,
|
||||||
type INodeIssues,
|
INodeIssues,
|
||||||
type INodeParameters,
|
INodeParameters,
|
||||||
type INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
type IPinData,
|
IPinData,
|
||||||
type IRunExecutionData,
|
IRunExecutionData,
|
||||||
type IRun,
|
IRun,
|
||||||
type IRunData,
|
IRunData,
|
||||||
type ITaskData,
|
ITaskData,
|
||||||
type IWorkflowSettings as IWorkflowSettingsWorkflow,
|
IWorkflowSettings as IWorkflowSettingsWorkflow,
|
||||||
type WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
type PublicInstalledPackage,
|
PublicInstalledPackage,
|
||||||
type INodeTypeNameVersion,
|
INodeTypeNameVersion,
|
||||||
type ILoadOptions,
|
ILoadOptions,
|
||||||
type INodeCredentials,
|
INodeCredentials,
|
||||||
type INodeListSearchItems,
|
INodeListSearchItems,
|
||||||
type NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
type IDisplayOptions,
|
IDisplayOptions,
|
||||||
type ExecutionSummary,
|
ExecutionSummary,
|
||||||
type FeatureFlags,
|
FeatureFlags,
|
||||||
type ExecutionStatus,
|
ExecutionStatus,
|
||||||
type ITelemetryTrackProperties,
|
ITelemetryTrackProperties,
|
||||||
type IUserManagementSettings,
|
IUserManagementSettings,
|
||||||
type WorkflowSettings,
|
WorkflowSettings,
|
||||||
type IUserSettings,
|
IUserSettings,
|
||||||
type IN8nUISettings,
|
IN8nUISettings,
|
||||||
type BannerName,
|
BannerName,
|
||||||
type INodeExecutionData,
|
INodeExecutionData,
|
||||||
type INodeProperties,
|
INodeProperties,
|
||||||
type NodeConnectionType,
|
NodeConnectionType,
|
||||||
type INodeCredentialsDetails,
|
INodeCredentialsDetails,
|
||||||
type StartNodeData,
|
StartNodeData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { BulkCommand, Undoable } from '@/models/history';
|
import type { BulkCommand, Undoable } from '@/models/history';
|
||||||
import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
|
import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
|
||||||
@@ -1805,9 +1805,7 @@ export type AddedNode = {
|
|||||||
type: string;
|
type: string;
|
||||||
openDetail?: boolean;
|
openDetail?: boolean;
|
||||||
isAutoAdd?: boolean;
|
isAutoAdd?: boolean;
|
||||||
name?: string;
|
} & Partial<INodeUi>;
|
||||||
position?: XYPosition;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AddedNodeConnection = {
|
export type AddedNodeConnection = {
|
||||||
from: { nodeIndex: number; outputIndex?: number };
|
from: { nodeIndex: number; outputIndex?: number };
|
||||||
|
|||||||
@@ -48,10 +48,18 @@ const props = withDefaults(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getSelectedEdges, getSelectedNodes, viewportRef, fitView, project } = useVueFlow({
|
const { getSelectedEdges, getSelectedNodes, viewportRef, fitView, project, onPaneReady } =
|
||||||
id: props.id,
|
useVueFlow({
|
||||||
|
id: props.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
onPaneReady(async () => {
|
||||||
|
await onFitView();
|
||||||
|
paneReady.value = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const paneReady = ref(false);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nodes
|
* Nodes
|
||||||
*/
|
*/
|
||||||
@@ -183,7 +191,7 @@ function onClickPane(event: MouseEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onFitView() {
|
async function onFitView() {
|
||||||
await fitView();
|
await fitView({ maxZoom: 1.2, padding: 0.1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -207,12 +215,12 @@ onUnmounted(() => {
|
|||||||
:nodes="nodes"
|
:nodes="nodes"
|
||||||
:edges="connections"
|
:edges="connections"
|
||||||
:apply-changes="false"
|
:apply-changes="false"
|
||||||
fit-view-on-init
|
|
||||||
pan-on-scroll
|
pan-on-scroll
|
||||||
snap-to-grid
|
snap-to-grid
|
||||||
:snap-grid="[16, 16]"
|
:snap-grid="[16, 16]"
|
||||||
:min-zoom="0.2"
|
:min-zoom="0.2"
|
||||||
:max-zoom="2"
|
:max-zoom="4"
|
||||||
|
:class="[$style.canvas, { [$style.visible]: paneReady }]"
|
||||||
data-test-id="canvas"
|
data-test-id="canvas"
|
||||||
@node-drag-stop="onNodeDragStop"
|
@node-drag-stop="onNodeDragStop"
|
||||||
@selection-drag-stop="onSelectionDragStop"
|
@selection-drag-stop="onSelectionDragStop"
|
||||||
@@ -253,10 +261,21 @@ onUnmounted(() => {
|
|||||||
data-test-id="canvas-controls"
|
data-test-id="canvas-controls"
|
||||||
:class="$style.canvasControls"
|
:class="$style.canvasControls"
|
||||||
:position="controlsPosition"
|
:position="controlsPosition"
|
||||||
|
@fit-view="onFitView"
|
||||||
></Controls>
|
></Controls>
|
||||||
</VueFlow>
|
</VueFlow>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.canvas {
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.vue-flow__controls {
|
.vue-flow__controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -61,15 +61,114 @@ describe('useCanvasOperations', () => {
|
|||||||
const workflow = mock<IWorkflowDb>({
|
const workflow = mock<IWorkflowDb>({
|
||||||
id: workflowId,
|
id: workflowId,
|
||||||
nodes: [],
|
nodes: [],
|
||||||
|
connections: {},
|
||||||
tags: [],
|
tags: [],
|
||||||
usedCredentials: [],
|
usedCredentials: [],
|
||||||
});
|
});
|
||||||
workflowsStore.workflowsById[workflowId] = workflow;
|
|
||||||
|
workflowsStore.resetWorkflow();
|
||||||
|
workflowsStore.resetState();
|
||||||
await workflowHelpers.initState(workflow);
|
await workflowHelpers.initState(workflow);
|
||||||
|
|
||||||
canvasOperations = useCanvasOperations({ router, lastClickPosition });
|
canvasOperations = useCanvasOperations({ router, lastClickPosition });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('addNode', () => {
|
||||||
|
it('should throw error when node type does not exist', async () => {
|
||||||
|
vi.spyOn(nodeTypesStore, 'getNodeTypes').mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await expect(canvasOperations.addNode({ type: 'nonexistent' })).rejects.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create node with default version when version is undefined', async () => {
|
||||||
|
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]);
|
||||||
|
|
||||||
|
const result = await canvasOperations.addNode({
|
||||||
|
name: 'example',
|
||||||
|
type: 'type',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.typeVersion).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create node with last version when version is an array', async () => {
|
||||||
|
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type', version: [1, 2] })]);
|
||||||
|
|
||||||
|
const result = await canvasOperations.addNode({
|
||||||
|
type: 'type',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.typeVersion).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create node with default position when position is not provided', async () => {
|
||||||
|
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]);
|
||||||
|
|
||||||
|
const result = await canvasOperations.addNode({
|
||||||
|
type: 'type',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.position).toEqual([460, 460]); // Default last click position
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create node with provided position when position is provided', async () => {
|
||||||
|
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]);
|
||||||
|
|
||||||
|
const result = await canvasOperations.addNode({
|
||||||
|
type: 'type',
|
||||||
|
position: [20, 20],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.position).toEqual([20, 20]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create node with default credentials when only one credential is available', async () => {
|
||||||
|
const credential = mock<ICredentialsResponse>({ id: '1', name: 'cred', type: 'cred' });
|
||||||
|
const nodeTypeName = 'type';
|
||||||
|
|
||||||
|
nodeTypesStore.setNodeTypes([
|
||||||
|
mockNodeTypeDescription({ name: nodeTypeName, credentials: [{ name: credential.name }] }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
credentialsStore.addCredentials([credential]);
|
||||||
|
|
||||||
|
// @ts-expect-error Known pinia issue when spying on store getters
|
||||||
|
vi.spyOn(credentialsStore, 'getUsableCredentialByType', 'get').mockReturnValue(() => [
|
||||||
|
credential,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await canvasOperations.addNode({
|
||||||
|
type: nodeTypeName,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.credentials).toEqual({ [credential.name]: { id: '1', name: credential.name } });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not assign credentials when multiple credentials are available', async () => {
|
||||||
|
const credentialA = mock<ICredentialsResponse>({ id: '1', name: 'credA', type: 'cred' });
|
||||||
|
const credentialB = mock<ICredentialsResponse>({ id: '1', name: 'credB', type: 'cred' });
|
||||||
|
const nodeTypeName = 'type';
|
||||||
|
|
||||||
|
nodeTypesStore.setNodeTypes([
|
||||||
|
mockNodeTypeDescription({
|
||||||
|
name: nodeTypeName,
|
||||||
|
credentials: [{ name: credentialA.name }, { name: credentialB.name }],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// @ts-expect-error Known pinia issue when spying on store getters
|
||||||
|
vi.spyOn(credentialsStore, 'getUsableCredentialByType', 'get').mockReturnValue(() => [
|
||||||
|
credentialA,
|
||||||
|
credentialB,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await canvasOperations.addNode({
|
||||||
|
type: 'type',
|
||||||
|
});
|
||||||
|
expect(result.credentials).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('updateNodePosition', () => {
|
describe('updateNodePosition', () => {
|
||||||
it('should update node position', () => {
|
it('should update node position', () => {
|
||||||
const setNodePositionByIdSpy = vi
|
const setNodePositionByIdSpy = vi
|
||||||
@@ -123,104 +222,6 @@ describe('useCanvasOperations', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('initializeNodeDataWithDefaultCredentials', () => {
|
|
||||||
it('should throw error when node type does not exist', async () => {
|
|
||||||
vi.spyOn(nodeTypesStore, 'getNodeTypes').mockResolvedValue(undefined);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
canvasOperations.initializeNodeDataWithDefaultCredentials({ type: 'nonexistent' }),
|
|
||||||
).rejects.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create node with default version when version is undefined', async () => {
|
|
||||||
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]);
|
|
||||||
|
|
||||||
const result = await canvasOperations.initializeNodeDataWithDefaultCredentials({
|
|
||||||
name: 'example',
|
|
||||||
type: 'type',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.typeVersion).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create node with last version when version is an array', async () => {
|
|
||||||
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type', version: [1, 2] })]);
|
|
||||||
|
|
||||||
const result = await canvasOperations.initializeNodeDataWithDefaultCredentials({
|
|
||||||
type: 'type',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.typeVersion).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create node with default position when position is not provided', async () => {
|
|
||||||
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]);
|
|
||||||
|
|
||||||
const result = await canvasOperations.initializeNodeDataWithDefaultCredentials({
|
|
||||||
type: 'type',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.position).toEqual([0, 0]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create node with provided position when position is provided', async () => {
|
|
||||||
nodeTypesStore.setNodeTypes([mockNodeTypeDescription({ name: 'type' })]);
|
|
||||||
|
|
||||||
const result = await canvasOperations.initializeNodeDataWithDefaultCredentials({
|
|
||||||
type: 'type',
|
|
||||||
position: [10, 20],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.position).toEqual([10, 20]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create node with default credentials when only one credential is available', async () => {
|
|
||||||
const credential = mock<ICredentialsResponse>({ id: '1', name: 'cred', type: 'cred' });
|
|
||||||
const nodeTypeName = 'type';
|
|
||||||
|
|
||||||
nodeTypesStore.setNodeTypes([
|
|
||||||
mockNodeTypeDescription({ name: nodeTypeName, credentials: [{ name: credential.name }] }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
credentialsStore.addCredentials([credential]);
|
|
||||||
|
|
||||||
// @ts-expect-error Known pinia issue when spying on store getters
|
|
||||||
vi.spyOn(credentialsStore, 'getUsableCredentialByType', 'get').mockReturnValue(() => [
|
|
||||||
credential,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await canvasOperations.initializeNodeDataWithDefaultCredentials({
|
|
||||||
type: nodeTypeName,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.credentials).toEqual({ [credential.name]: { id: '1', name: credential.name } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not assign credentials when multiple credentials are available', async () => {
|
|
||||||
const credentialA = mock<ICredentialsResponse>({ id: '1', name: 'credA', type: 'cred' });
|
|
||||||
const credentialB = mock<ICredentialsResponse>({ id: '1', name: 'credB', type: 'cred' });
|
|
||||||
const nodeTypeName = 'type';
|
|
||||||
|
|
||||||
nodeTypesStore.setNodeTypes([
|
|
||||||
mockNodeTypeDescription({
|
|
||||||
name: nodeTypeName,
|
|
||||||
credentials: [{ name: credentialA.name }, { name: credentialB.name }],
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// @ts-expect-error Known pinia issue when spying on store getters
|
|
||||||
vi.spyOn(credentialsStore, 'getUsableCredentialByType', 'get').mockReturnValue(() => [
|
|
||||||
credentialA,
|
|
||||||
credentialB,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const result = await canvasOperations.initializeNodeDataWithDefaultCredentials({
|
|
||||||
type: 'type',
|
|
||||||
});
|
|
||||||
expect(result.credentials).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('addNodes', () => {
|
describe('addNodes', () => {
|
||||||
it('should add nodes at specified positions', async () => {
|
it('should add nodes at specified positions', async () => {
|
||||||
const nodeTypeName = 'type';
|
const nodeTypeName = 'type';
|
||||||
@@ -489,6 +490,7 @@ describe('useCanvasOperations', () => {
|
|||||||
const nodes = [
|
const nodes = [
|
||||||
mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [40, 40] }),
|
mockNode({ id: 'a', name: 'Node A', type: nodeTypeName, position: [40, 40] }),
|
||||||
mockNode({ id: 'b', name: 'Node B', type: nodeTypeName, position: [40, 40] }),
|
mockNode({ id: 'b', name: 'Node B', type: nodeTypeName, position: [40, 40] }),
|
||||||
|
mockNode({ id: 'c', name: 'Node C', type: nodeTypeName, position: [40, 40] }),
|
||||||
];
|
];
|
||||||
|
|
||||||
nodeTypesStore.setNodeTypes([
|
nodeTypesStore.setNodeTypes([
|
||||||
@@ -504,14 +506,27 @@ describe('useCanvasOperations', () => {
|
|||||||
.mockReturnValueOnce(nodes[1]);
|
.mockReturnValueOnce(nodes[1]);
|
||||||
|
|
||||||
const connections = [
|
const connections = [
|
||||||
{ from: { nodeIndex: 0, outputIndex: 0 }, to: { nodeIndex: 1, inputIndex: 0 } },
|
{
|
||||||
{ from: { nodeIndex: 1, outputIndex: 0 }, to: { nodeIndex: 2, inputIndex: 0 } },
|
source: nodes[0].id,
|
||||||
|
target: nodes[1].id,
|
||||||
|
data: {
|
||||||
|
source: { type: NodeConnectionType.Main, index: 0 },
|
||||||
|
target: { type: NodeConnectionType.Main, index: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: nodes[1].id,
|
||||||
|
target: nodes[2].id,
|
||||||
|
data: {
|
||||||
|
source: { type: NodeConnectionType.Main, index: 0 },
|
||||||
|
target: { type: NodeConnectionType.Main, index: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const offsetIndex = 0;
|
|
||||||
|
|
||||||
const addConnectionSpy = vi.spyOn(workflowsStore, 'addConnection');
|
const addConnectionSpy = vi.spyOn(workflowsStore, 'addConnection');
|
||||||
|
|
||||||
await canvasOperations.addConnections(connections, { offsetIndex });
|
await canvasOperations.addConnections(connections);
|
||||||
|
|
||||||
expect(addConnectionSpy).toHaveBeenCalledWith({
|
expect(addConnectionSpy).toHaveBeenCalledWith({
|
||||||
connection: [
|
connection: [
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1261,6 +1261,7 @@ export function useNodeHelpers() {
|
|||||||
deleteJSPlumbConnection,
|
deleteJSPlumbConnection,
|
||||||
loadNodesProperties,
|
loadNodesProperties,
|
||||||
addNodes,
|
addNodes,
|
||||||
|
addConnections,
|
||||||
addConnection,
|
addConnection,
|
||||||
removeConnection,
|
removeConnection,
|
||||||
removeConnectionByConnectionInfo,
|
removeConnectionByConnectionInfo,
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ 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';
|
import { useTagsStore } from '@/stores/tags.store';
|
||||||
|
import useWorkflowsEEStore from '@/stores/workflows.ee.store';
|
||||||
|
|
||||||
export function resolveParameter<T = IDataObject>(
|
export function resolveParameter<T = IDataObject>(
|
||||||
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
|
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
|
||||||
@@ -439,6 +440,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
|||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const templatesStore = useTemplatesStore();
|
const templatesStore = useTemplatesStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const workflowsEEStore = useWorkflowsEEStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
@@ -1063,6 +1065,17 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
|||||||
workflowsStore.setWorkflowVersionId(workflowData.versionId);
|
workflowsStore.setWorkflowVersionId(workflowData.versionId);
|
||||||
workflowsStore.setWorkflowMetadata(workflowData.meta);
|
workflowsStore.setWorkflowMetadata(workflowData.meta);
|
||||||
|
|
||||||
|
if (workflowData.usedCredentials) {
|
||||||
|
workflowsStore.setUsedCredentials(workflowData.usedCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workflowData.sharedWithProjects) {
|
||||||
|
workflowsEEStore.setWorkflowSharedWith({
|
||||||
|
workflowId: workflowData.id,
|
||||||
|
sharedWithProjects: workflowData.sharedWithProjects,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const tags = (workflowData.tags ?? []) as ITag[];
|
const tags = (workflowData.tags ?? []) as ITag[];
|
||||||
const tagIds = tags.map((tag) => tag.id);
|
const tagIds = tags.map((tag) => tag.id);
|
||||||
workflowsStore.setWorkflowTagIds(tagIds || []);
|
workflowsStore.setWorkflowTagIds(tagIds || []);
|
||||||
|
|||||||
@@ -203,8 +203,10 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
|||||||
// canvasStore.newNodeInsertPosition = null;
|
// canvasStore.newNodeInsertPosition = null;
|
||||||
|
|
||||||
if (isVueFlowConnection(connection)) {
|
if (isVueFlowConnection(connection)) {
|
||||||
uiStore.lastSelectedNodeConnection = connection;
|
uiStore.lastInteractedWithNodeConnection = connection;
|
||||||
}
|
}
|
||||||
|
uiStore.lastInteractedWithNodeHandle = connection.sourceHandle ?? null;
|
||||||
|
uiStore.lastInteractedWithNodeId = sourceNode.id;
|
||||||
|
|
||||||
openNodeCreator({
|
openNodeCreator({
|
||||||
source: eventSource,
|
source: eventSource,
|
||||||
|
|||||||
@@ -178,7 +178,6 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||||||
const lastSelectedNode = ref<string | null>(null);
|
const lastSelectedNode = ref<string | null>(null);
|
||||||
const lastSelectedNodeOutputIndex = ref<number | null>(null);
|
const lastSelectedNodeOutputIndex = ref<number | null>(null);
|
||||||
const lastSelectedNodeEndpointUuid = ref<string | null>(null);
|
const lastSelectedNodeEndpointUuid = ref<string | null>(null);
|
||||||
const lastSelectedNodeConnection = ref<Connection | null>(null);
|
|
||||||
const nodeViewOffsetPosition = ref<[number, number]>([0, 0]);
|
const nodeViewOffsetPosition = ref<[number, number]>([0, 0]);
|
||||||
const nodeViewMoveInProgress = ref<boolean>(false);
|
const nodeViewMoveInProgress = ref<boolean>(false);
|
||||||
const selectedNodes = ref<INodeUi[]>([]);
|
const selectedNodes = ref<INodeUi[]>([]);
|
||||||
@@ -189,6 +188,11 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||||||
const pendingNotificationsForViews = ref<{ [key in VIEWS]?: NotificationOptions[] }>({});
|
const pendingNotificationsForViews = ref<{ [key in VIEWS]?: NotificationOptions[] }>({});
|
||||||
const isCreateNodeActive = ref<boolean>(false);
|
const isCreateNodeActive = ref<boolean>(false);
|
||||||
|
|
||||||
|
// Last interacted with - Canvas v2 specific
|
||||||
|
const lastInteractedWithNodeConnection = ref<Connection | null>(null);
|
||||||
|
const lastInteractedWithNodeHandle = ref<string | null>(null);
|
||||||
|
const lastInteractedWithNodeId = ref<string | null>(null);
|
||||||
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
@@ -275,6 +279,14 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const lastInteractedWithNode = computed(() => {
|
||||||
|
if (lastInteractedWithNodeId.value) {
|
||||||
|
return workflowsStore.getNodeById(lastInteractedWithNodeId.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const isVersionsOpen = computed(() => {
|
const isVersionsOpen = computed(() => {
|
||||||
return modalsById.value[VERSIONS_MODAL_KEY].open;
|
return modalsById.value[VERSIONS_MODAL_KEY].open;
|
||||||
});
|
});
|
||||||
@@ -600,6 +612,12 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||||||
delete pendingNotificationsForViews.value[view];
|
delete pendingNotificationsForViews.value[view];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resetLastInteractedWith() {
|
||||||
|
lastInteractedWithNodeConnection.value = null;
|
||||||
|
lastInteractedWithNodeHandle.value = null;
|
||||||
|
lastInteractedWithNodeId.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appliedTheme,
|
appliedTheme,
|
||||||
logo,
|
logo,
|
||||||
@@ -621,7 +639,10 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||||||
selectedNodes,
|
selectedNodes,
|
||||||
bannersHeight,
|
bannersHeight,
|
||||||
lastSelectedNodeEndpointUuid,
|
lastSelectedNodeEndpointUuid,
|
||||||
lastSelectedNodeConnection,
|
lastInteractedWithNodeConnection,
|
||||||
|
lastInteractedWithNodeHandle,
|
||||||
|
lastInteractedWithNodeId,
|
||||||
|
lastInteractedWithNode,
|
||||||
nodeViewOffsetPosition,
|
nodeViewOffsetPosition,
|
||||||
nodeViewMoveInProgress,
|
nodeViewMoveInProgress,
|
||||||
nodeViewInitialized,
|
nodeViewInitialized,
|
||||||
@@ -670,6 +691,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||||||
clearBannerStack,
|
clearBannerStack,
|
||||||
setNotificationsForView,
|
setNotificationsForView,
|
||||||
deleteNotificationsForView,
|
deleteNotificationsForView,
|
||||||
|
resetLastInteractedWith,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ 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 type { ProjectSharingData } from '@/types/projects.types';
|
||||||
|
|
||||||
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
|
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
|
||||||
name: '',
|
name: '',
|
||||||
@@ -452,6 +453,15 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||||||
return workflowData;
|
return workflowData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeNewWorkflowShareable() {
|
||||||
|
const { currentProject, personalProject } = useProjectsStore();
|
||||||
|
const homeProject = currentProject ?? personalProject ?? {};
|
||||||
|
const scopes = currentProject?.scopes ?? personalProject?.scopes ?? [];
|
||||||
|
|
||||||
|
workflow.value.homeProject = homeProject as ProjectSharingData;
|
||||||
|
workflow.value.scopes = scopes;
|
||||||
|
}
|
||||||
|
|
||||||
function resetWorkflow() {
|
function resetWorkflow() {
|
||||||
workflow.value = createEmptyWorkflow();
|
workflow.value = createEmptyWorkflow();
|
||||||
}
|
}
|
||||||
@@ -1589,6 +1599,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||||||
fetchAllWorkflows,
|
fetchAllWorkflows,
|
||||||
fetchWorkflow,
|
fetchWorkflow,
|
||||||
getNewWorkflowData,
|
getNewWorkflowData,
|
||||||
|
makeNewWorkflowShareable,
|
||||||
resetWorkflow,
|
resetWorkflow,
|
||||||
resetState,
|
resetState,
|
||||||
addExecutingNode,
|
addExecutingNode,
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
import type {
|
import type {
|
||||||
ConnectionTypes,
|
ConnectionTypes,
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
|
IConnection,
|
||||||
INodeConnections,
|
INodeConnections,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
|
|
||||||
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
|
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
|
||||||
import type { INodeUi } from '@/Interface';
|
import type { INodeUi } from '@/Interface';
|
||||||
import type { ComputedRef, Ref } from 'vue';
|
import type { ComputedRef, Ref } from 'vue';
|
||||||
|
import type { PartialBy } from '@/utils/typeHelpers';
|
||||||
|
|
||||||
export type CanvasConnectionPortType = ConnectionTypes;
|
export type CanvasConnectionPortType = ConnectionTypes;
|
||||||
|
|
||||||
@@ -107,13 +108,14 @@ export interface CanvasConnectionData {
|
|||||||
|
|
||||||
export type CanvasConnection = DefaultEdge<CanvasConnectionData>;
|
export type CanvasConnection = DefaultEdge<CanvasConnectionData>;
|
||||||
|
|
||||||
export interface CanvasPluginContext {
|
export type CanvasConnectionCreateData = {
|
||||||
instance: BrowserJsPlumbInstance;
|
source: string;
|
||||||
}
|
target: string;
|
||||||
|
data: {
|
||||||
export interface CanvasPlugin {
|
source: PartialBy<IConnection, 'node'>;
|
||||||
(ctx: CanvasPluginContext): void;
|
target: PartialBy<IConnection, 'node'>;
|
||||||
}
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export interface CanvasNodeInjectionData {
|
export interface CanvasNodeInjectionData {
|
||||||
id: Ref<string>;
|
id: Ref<string>;
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import type { Connection } from '@vue-flow/core';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { isValidCanvasConnectionMode, isValidNodeConnectionType } from '@/utils/typeGuards';
|
import { isValidCanvasConnectionMode, isValidNodeConnectionType } from '@/utils/typeGuards';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
import type { Connection as VueFlowConnection } from '@vue-flow/core/dist/types/connection';
|
|
||||||
import { PUSH_NODES_OFFSET } from '@/utils/nodeViewUtils';
|
|
||||||
|
|
||||||
export function mapLegacyConnectionsToCanvasConnections(
|
export function mapLegacyConnectionsToCanvasConnections(
|
||||||
legacyConnections: IConnections,
|
legacyConnections: IConnections,
|
||||||
@@ -94,21 +92,6 @@ export function parseCanvasConnectionHandleString(handle: string | null | undefi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the width and height of a connection
|
|
||||||
*
|
|
||||||
* @TODO See whether this is actually needed or just a legacy jsPlumb check
|
|
||||||
*/
|
|
||||||
export function getVueFlowConnectorLengths(connection: VueFlowConnection): [number, number] {
|
|
||||||
const connectionId = createCanvasConnectionId(connection);
|
|
||||||
const edgeRef = document.getElementById(connectionId);
|
|
||||||
if (!edgeRef) {
|
|
||||||
return [PUSH_NODES_OFFSET, PUSH_NODES_OFFSET];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [edgeRef.clientWidth, edgeRef.clientHeight];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createCanvasConnectionHandleString({
|
export function createCanvasConnectionHandleString({
|
||||||
mode,
|
mode,
|
||||||
type = NodeConnectionType.Main,
|
type = NodeConnectionType.Main,
|
||||||
|
|||||||
@@ -28,10 +28,9 @@ import type {
|
|||||||
XYPosition,
|
XYPosition,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
import type { Connection } from '@vue-flow/core';
|
import type { Connection } from '@vue-flow/core';
|
||||||
import type { CanvasNode, ConnectStartEvent } from '@/types';
|
import type { CanvasConnectionCreateData, CanvasNode, ConnectStartEvent } from '@/types';
|
||||||
import { CanvasNodeRenderType } from '@/types';
|
import { CanvasNodeRenderType } from '@/types';
|
||||||
import {
|
import {
|
||||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
|
|
||||||
CHAT_TRIGGER_NODE_TYPE,
|
CHAT_TRIGGER_NODE_TYPE,
|
||||||
EnterpriseEditionFeature,
|
EnterpriseEditionFeature,
|
||||||
MAIN_HEADER_TABS,
|
MAIN_HEADER_TABS,
|
||||||
@@ -46,13 +45,8 @@ import {
|
|||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { TelemetryHelpers } from 'n8n-workflow';
|
import { TelemetryHelpers, NodeConnectionType } from 'n8n-workflow';
|
||||||
import type {
|
import type { IDataObject, ExecutionSummary, IConnection, IWorkflowBase } from 'n8n-workflow';
|
||||||
NodeConnectionType,
|
|
||||||
ExecutionSummary,
|
|
||||||
IConnection,
|
|
||||||
IWorkflowBase,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
@@ -70,11 +64,8 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
|||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { useHistoryStore } from '@/stores/history.store';
|
import { useHistoryStore } from '@/stores/history.store';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
|
||||||
import useWorkflowsEEStore from '@/stores/workflows.ee.store';
|
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
import { useExecutionDebugging } from '@/composables/useExecutionDebugging';
|
import { useExecutionDebugging } from '@/composables/useExecutionDebugging';
|
||||||
import type { ProjectSharingData } from '@/types/projects.types';
|
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { sourceControlEventBus } from '@/event-bus/source-control';
|
import { sourceControlEventBus } from '@/event-bus/source-control';
|
||||||
import { useTagsStore } from '@/stores/tags.store';
|
import { useTagsStore } from '@/stores/tags.store';
|
||||||
@@ -85,6 +76,7 @@ import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/butto
|
|||||||
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
|
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
|
||||||
import CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue';
|
import CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue';
|
||||||
import { nodeViewEventBus } from '@/event-bus';
|
import { nodeViewEventBus } from '@/event-bus';
|
||||||
|
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
||||||
import { tryToParseNumber } from '@/utils/typesUtils';
|
import { tryToParseNumber } from '@/utils/typesUtils';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { createEventBus } from 'n8n-design-system';
|
import { createEventBus } from 'n8n-design-system';
|
||||||
@@ -105,15 +97,13 @@ const telemetry = useTelemetry();
|
|||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const titleChange = useTitleChange();
|
const { titleReset, titleSet } = useTitleChange();
|
||||||
const workflowHelpers = useWorkflowHelpers({ router });
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const posthog = usePostHog();
|
|
||||||
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const workflowsEEStore = useWorkflowsEEStore();
|
|
||||||
const sourceControlStore = useSourceControlStore();
|
const sourceControlStore = useSourceControlStore();
|
||||||
const nodeCreatorStore = useNodeCreatorStore();
|
const nodeCreatorStore = useNodeCreatorStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
@@ -153,6 +143,10 @@ const {
|
|||||||
revertDeleteConnection,
|
revertDeleteConnection,
|
||||||
setNodeActiveByName,
|
setNodeActiveByName,
|
||||||
addConnections,
|
addConnections,
|
||||||
|
importWorkflowData,
|
||||||
|
fetchWorkflowDataFromUrl,
|
||||||
|
resetWorkspace,
|
||||||
|
initializeWorkspace,
|
||||||
editableWorkflow,
|
editableWorkflow,
|
||||||
editableWorkflowObject,
|
editableWorkflowObject,
|
||||||
} = useCanvasOperations({ router, lastClickPosition });
|
} = useCanvasOperations({ router, lastClickPosition });
|
||||||
@@ -202,12 +196,6 @@ const fallbackNodes = computed<INodeUi[]>(() =>
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async function initializeData() {
|
async function initializeData() {
|
||||||
isLoading.value = true;
|
|
||||||
canvasStore.startLoading();
|
|
||||||
|
|
||||||
resetWorkspace();
|
|
||||||
titleChange.titleReset();
|
|
||||||
|
|
||||||
const loadPromises = (() => {
|
const loadPromises = (() => {
|
||||||
if (settingsStore.isPreviewMode && isDemoRoute.value) return [];
|
if (settingsStore.isPreviewMode && isDemoRoute.value) return [];
|
||||||
|
|
||||||
@@ -232,9 +220,6 @@ async function initializeData() {
|
|||||||
return promises;
|
return promises;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// @TODO Implement this
|
|
||||||
// this.clipboard.onPaste.value = this.onClipboardPasteEvent;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(loadPromises);
|
await Promise.all(loadPromises);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -244,23 +229,10 @@ async function initializeData() {
|
|||||||
i18n.baseText('nodeView.showError.mounted1.message') + ':',
|
i18n.baseText('nodeView.showError.mounted1.message') + ':',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} finally {
|
|
||||||
canvasStore.stopLoading();
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
void usersStore.showPersonalizationSurvey();
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// @TODO: This currently breaks since front-end hooks are still not updated to work with pinia store
|
|
||||||
void externalHooks.run('nodeView.mount').catch(() => {});
|
|
||||||
|
|
||||||
// @TODO maybe we can find a better way to handle this
|
|
||||||
canvasStore.isDemo = isDemoRoute.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeView() {
|
async function initializeRoute() {
|
||||||
// In case the workflow got saved we do not have to run init
|
// In case the workflow got saved we do not have to run init
|
||||||
// as only the route changed but all the needed data is already loaded
|
// as only the route changed but all the needed data is already loaded
|
||||||
if (route.params.action === 'workflowSave') {
|
if (route.params.action === 'workflowSave') {
|
||||||
@@ -283,27 +255,12 @@ async function initializeView() {
|
|||||||
// If there is no workflow id, treat it as a new workflow
|
// If there is no workflow id, treat it as a new workflow
|
||||||
if (!workflowId.value || isNewWorkflowRoute.value) {
|
if (!workflowId.value || isNewWorkflowRoute.value) {
|
||||||
if (route.meta?.nodeView === true) {
|
if (route.meta?.nodeView === true) {
|
||||||
await initializeViewForNewWorkflow();
|
await initializeWorkspaceForNewWorkflow();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load workflow data
|
await initializeWorkspaceForExistingWorkflow(workflowId.value);
|
||||||
try {
|
|
||||||
await workflowsStore.fetchWorkflow(workflowId.value);
|
|
||||||
|
|
||||||
titleChange.titleSet(workflow.value.name, 'IDLE');
|
|
||||||
await openWorkflow(workflow.value);
|
|
||||||
await checkAndInitDebugMode();
|
|
||||||
|
|
||||||
trackOpenWorkflowFromOnboardingTemplate();
|
|
||||||
} catch (error) {
|
|
||||||
toast.showError(error, i18n.baseText('openWorkflow.workflowNotFoundError'));
|
|
||||||
|
|
||||||
void router.push({
|
|
||||||
name: VIEWS.NEW_WORKFLOW,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeHelpers.updateNodesInputIssues();
|
nodeHelpers.updateNodesInputIssues();
|
||||||
@@ -311,67 +268,40 @@ async function initializeView() {
|
|||||||
nodeHelpers.updateNodesParameterIssues();
|
nodeHelpers.updateNodesParameterIssues();
|
||||||
|
|
||||||
await loadCredentials();
|
await loadCredentials();
|
||||||
canvasEventBus.emit('fitView');
|
|
||||||
|
|
||||||
uiStore.nodeViewInitialized = true;
|
|
||||||
|
|
||||||
// Once view is initialized, pick up all toast notifications
|
|
||||||
// waiting in the store and display them
|
|
||||||
toast.showNotificationForViews([VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initializeViewForNewWorkflow() {
|
async function initializeWorkspaceForNewWorkflow() {
|
||||||
resetWorkspace();
|
resetWorkspace();
|
||||||
|
|
||||||
await workflowsStore.getNewWorkflowData(undefined, projectsStore.currentProjectId);
|
await workflowsStore.getNewWorkflowData(undefined, projectsStore.currentProjectId);
|
||||||
|
workflowsStore.makeNewWorkflowShareable();
|
||||||
|
|
||||||
workflowsStore.currentWorkflowExecutions = [];
|
|
||||||
executionsStore.activeExecution = null;
|
|
||||||
uiStore.stateIsDirty = false;
|
|
||||||
uiStore.nodeViewInitialized = true;
|
uiStore.nodeViewInitialized = true;
|
||||||
executionsStore.activeExecution = null;
|
|
||||||
|
|
||||||
makeNewWorkflowShareable();
|
|
||||||
await runAutoAddManualTriggerExperiment();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async function initializeWorkspaceForExistingWorkflow(id: string) {
|
||||||
* Pre-populate the canvas with the manual trigger node
|
resetWorkspace();
|
||||||
* if the experiment is enabled and the user is in the variant group
|
|
||||||
*/
|
try {
|
||||||
async function runAutoAddManualTriggerExperiment() {
|
const workflowData = await workflowsStore.fetchWorkflow(id);
|
||||||
if (
|
|
||||||
posthog.getVariant(CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name) !==
|
await openWorkflow(workflowData);
|
||||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.variant
|
await initializeDebugMode();
|
||||||
) {
|
|
||||||
return;
|
if (workflowData.meta?.onboardingId) {
|
||||||
|
trackOpenWorkflowFromOnboardingTemplate();
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflow.value.homeProject);
|
||||||
|
} catch (error) {
|
||||||
|
toast.showError(error, i18n.baseText('openWorkflow.workflowNotFoundError'));
|
||||||
|
|
||||||
|
void router.push({
|
||||||
|
name: VIEWS.NEW_WORKFLOW,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
uiStore.nodeViewInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const manualTriggerNode = canvasStore.getAutoAddManualTriggerNode();
|
|
||||||
if (manualTriggerNode) {
|
|
||||||
await addNodes([manualTriggerNode]);
|
|
||||||
uiStore.lastSelectedNode = manualTriggerNode.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetWorkspace() {
|
|
||||||
onOpenNodeCreator({ createNodeActive: false });
|
|
||||||
nodeCreatorStore.setShowScrim(false);
|
|
||||||
|
|
||||||
// Make sure that if there is a waiting test-webhook that it gets removed
|
|
||||||
if (isExecutionWaitingForWebhook.value) {
|
|
||||||
try {
|
|
||||||
void workflowsStore.removeTestWebhook(workflowsStore.workflowId);
|
|
||||||
} catch (error) {}
|
|
||||||
}
|
|
||||||
workflowsStore.resetWorkflow();
|
|
||||||
workflowsStore.resetState();
|
|
||||||
|
|
||||||
uiStore.removeActiveAction('workflowRunning');
|
|
||||||
uiStore.resetSelectedNodes();
|
|
||||||
uiStore.nodeViewOffsetPosition = [0, 0]; // @TODO Not sure if needed
|
|
||||||
|
|
||||||
// this.credentialsUpdated = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -379,65 +309,38 @@ function resetWorkspace() {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
async function openWorkflow(data: IWorkflowDb) {
|
async function openWorkflow(data: IWorkflowDb) {
|
||||||
const selectedExecution = executionsStore.activeExecution;
|
|
||||||
|
|
||||||
resetWorkspace();
|
resetWorkspace();
|
||||||
|
titleSet(workflow.value.name, 'IDLE');
|
||||||
|
|
||||||
await workflowHelpers.initState(data);
|
await initializeWorkspace(data);
|
||||||
await addNodes(data.nodes);
|
|
||||||
workflowsStore.setConnections(data.connections);
|
|
||||||
|
|
||||||
if (data.sharedWithProjects) {
|
|
||||||
workflowsEEStore.setWorkflowSharedWith({
|
|
||||||
workflowId: data.id,
|
|
||||||
sharedWithProjects: data.sharedWithProjects,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.usedCredentials) {
|
|
||||||
workflowsStore.setUsedCredentials(data.usedCredentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!nodeHelpers.credentialsUpdated.value) {
|
|
||||||
uiStore.stateIsDirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void externalHooks.run('workflow.open', {
|
void externalHooks.run('workflow.open', {
|
||||||
workflowId: data.id,
|
workflowId: data.id,
|
||||||
workflowName: data.name,
|
workflowName: data.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (selectedExecution?.workflowId !== data.id) {
|
// @TODO Check why this is needed when working on executions
|
||||||
executionsStore.activeExecution = null;
|
// const selectedExecution = executionsStore.activeExecution;
|
||||||
workflowsStore.currentWorkflowExecutions = [];
|
// if (selectedExecution?.workflowId !== data.id) {
|
||||||
} else {
|
// executionsStore.activeExecution = null;
|
||||||
executionsStore.activeExecution = selectedExecution;
|
// workflowsStore.currentWorkflowExecutions = [];
|
||||||
}
|
// } else {
|
||||||
|
// executionsStore.activeExecution = selectedExecution;
|
||||||
|
// }
|
||||||
|
|
||||||
await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflow.value.homeProject);
|
fitView();
|
||||||
}
|
}
|
||||||
|
|
||||||
function trackOpenWorkflowFromOnboardingTemplate() {
|
function trackOpenWorkflowFromOnboardingTemplate() {
|
||||||
if (workflow.value.meta?.onboardingId) {
|
telemetry.track(
|
||||||
telemetry.track(
|
`User opened workflow from onboarding template with ID ${workflow.value.meta?.onboardingId}`,
|
||||||
`User opened workflow from onboarding template with ID ${workflow.value.meta.onboardingId}`,
|
{
|
||||||
{
|
workflow_id: workflowId.value,
|
||||||
workflow_id: workflowId.value,
|
},
|
||||||
},
|
{
|
||||||
{
|
withPostHog: true,
|
||||||
withPostHog: true,
|
},
|
||||||
},
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeNewWorkflowShareable() {
|
|
||||||
const { currentProject, personalProject } = projectsStore;
|
|
||||||
const homeProject = currentProject ?? personalProject ?? {};
|
|
||||||
const scopes = currentProject?.scopes ?? personalProject?.scopes ?? [];
|
|
||||||
|
|
||||||
workflowsStore.workflow.homeProject = homeProject as ProjectSharingData;
|
|
||||||
workflowsStore.workflow.scopes = scopes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -485,7 +388,6 @@ async function openWorkflowTemplate(templateId: string) {
|
|||||||
|
|
||||||
uiStore.stateIsDirty = true;
|
uiStore.stateIsDirty = true;
|
||||||
|
|
||||||
canvasEventBus.emit('fitView');
|
|
||||||
canvasStore.stopLoading();
|
canvasStore.stopLoading();
|
||||||
|
|
||||||
void externalHooks.run('template.open', {
|
void externalHooks.run('template.open', {
|
||||||
@@ -493,6 +395,8 @@ async function openWorkflowTemplate(templateId: string) {
|
|||||||
templateName: data.name,
|
templateName: data.name,
|
||||||
workflow: data.workflow,
|
workflow: data.workflow,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fitView();
|
||||||
}
|
}
|
||||||
|
|
||||||
function trackOpenWorkflowTemplate(templateId: string) {
|
function trackOpenWorkflowTemplate(templateId: string) {
|
||||||
@@ -631,8 +535,46 @@ function onRevertDeleteConnection({ connection }: { connection: [IConnection, IC
|
|||||||
* Import / Export
|
* Import / Export
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function importWorkflowExact(_workflow: IWorkflowDataUpdate) {
|
async function importWorkflowExact({ workflow: workflowData }: { workflow: IWorkflowDataUpdate }) {
|
||||||
// @TODO
|
if (!workflowData.nodes || !workflowData.connections) {
|
||||||
|
throw new Error('Invalid workflow object');
|
||||||
|
}
|
||||||
|
|
||||||
|
resetWorkspace();
|
||||||
|
|
||||||
|
await initializeWorkspace({
|
||||||
|
...workflowData,
|
||||||
|
nodes: NodeViewUtils.getFixedNodesList<INodeUi>(workflowData.nodes),
|
||||||
|
} as IWorkflowDb);
|
||||||
|
|
||||||
|
fitView();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onImportWorkflowDataEvent(data: IDataObject) {
|
||||||
|
await importWorkflowData(data.data as IWorkflowDataUpdate, 'file');
|
||||||
|
|
||||||
|
fitView();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onImportWorkflowUrlEvent(data: IDataObject) {
|
||||||
|
const workflowData = await fetchWorkflowDataFromUrl(data.url as string);
|
||||||
|
if (!workflowData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await importWorkflowData(workflowData, 'url');
|
||||||
|
|
||||||
|
fitView();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addImportEventBindings() {
|
||||||
|
nodeViewEventBus.on('importWorkflowData', onImportWorkflowDataEvent);
|
||||||
|
nodeViewEventBus.on('importWorkflowUrl', onImportWorkflowUrlEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeImportEventBindings() {
|
||||||
|
nodeViewEventBus.off('importWorkflowData', onImportWorkflowDataEvent);
|
||||||
|
nodeViewEventBus.off('importWorkflowUrl', onImportWorkflowUrlEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -649,11 +591,31 @@ async function onAddNodesAndConnections(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await addNodes(nodes, { dragAndDrop, position });
|
await addNodes(nodes, { dragAndDrop, position });
|
||||||
await addConnections(connections, {
|
|
||||||
offsetIndex: editableWorkflow.value.nodes.length - nodes.length,
|
const offsetIndex = editableWorkflow.value.nodes.length - nodes.length;
|
||||||
|
const mappedConnections: CanvasConnectionCreateData[] = connections.map(({ from, to }) => {
|
||||||
|
const fromNode = editableWorkflow.value.nodes[offsetIndex + from.nodeIndex];
|
||||||
|
const toNode = editableWorkflow.value.nodes[offsetIndex + to.nodeIndex];
|
||||||
|
|
||||||
|
return {
|
||||||
|
source: fromNode.id,
|
||||||
|
target: toNode.id,
|
||||||
|
data: {
|
||||||
|
source: {
|
||||||
|
index: from.outputIndex ?? 0,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
index: to.inputIndex ?? 0,
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
uiStore.lastSelectedNodeConnection = null;
|
await addConnections(mappedConnections);
|
||||||
|
|
||||||
|
uiStore.resetLastInteractedWith();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSwitchActiveNode(nodeName: string) {
|
async function onSwitchActiveNode(nodeName: string) {
|
||||||
@@ -865,7 +827,7 @@ async function onSourceControlPull() {
|
|||||||
if (workflowId.value !== null && !uiStore.stateIsDirty) {
|
if (workflowId.value !== null && !uiStore.stateIsDirty) {
|
||||||
const workflowData = await workflowsStore.fetchWorkflow(workflowId.value);
|
const workflowData = await workflowsStore.fetchWorkflow(workflowId.value);
|
||||||
if (workflowData) {
|
if (workflowData) {
|
||||||
titleChange.titleSet(workflowData.name, 'IDLE');
|
titleSet(workflowData.name, 'IDLE');
|
||||||
await openWorkflow(workflowData);
|
await openWorkflow(workflowData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -909,7 +871,7 @@ async function onPostMessageReceived(message: MessageEvent) {
|
|||||||
const json = JSON.parse(message.data);
|
const json = JSON.parse(message.data);
|
||||||
if (json && json.command === 'openWorkflow') {
|
if (json && json.command === 'openWorkflow') {
|
||||||
try {
|
try {
|
||||||
await importWorkflowExact(json.data);
|
await importWorkflowExact(json);
|
||||||
canOpenNDV.value = json.canOpenNDV ?? true;
|
canOpenNDV.value = json.canOpenNDV ?? true;
|
||||||
hideNodeIssues.value = json.hideNodeIssues ?? false;
|
hideNodeIssues.value = json.hideNodeIssues ?? false;
|
||||||
isExecutionPreview.value = false;
|
isExecutionPreview.value = false;
|
||||||
@@ -1009,9 +971,9 @@ function checkIfRouteIsAllowed() {
|
|||||||
* Debug mode
|
* Debug mode
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function checkAndInitDebugMode() {
|
async function initializeDebugMode() {
|
||||||
if (route.name === VIEWS.EXECUTION_DEBUG) {
|
if (route.name === VIEWS.EXECUTION_DEBUG) {
|
||||||
titleChange.titleSet(workflowsStore.workflowName, 'DEBUG');
|
titleSet(workflowsStore.workflowName, 'DEBUG');
|
||||||
if (!workflowsStore.isInDebugMode) {
|
if (!workflowsStore.isInDebugMode) {
|
||||||
await applyExecutionData(route.params.executionId as string);
|
await applyExecutionData(route.params.executionId as string);
|
||||||
workflowsStore.isInDebugMode = true;
|
workflowsStore.isInDebugMode = true;
|
||||||
@@ -1019,6 +981,14 @@ async function checkAndInitDebugMode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canvas
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fitView() {
|
||||||
|
setTimeout(() => canvasEventBus.emit('fitView'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mouse events
|
* Mouse events
|
||||||
*/
|
*/
|
||||||
@@ -1130,8 +1100,23 @@ onBeforeMount(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
canvasStore.startLoading();
|
||||||
|
titleReset();
|
||||||
|
resetWorkspace();
|
||||||
|
|
||||||
void initializeData().then(() => {
|
void initializeData().then(() => {
|
||||||
void initializeView();
|
void initializeRoute()
|
||||||
|
.then(() => {
|
||||||
|
// Once view is initialized, pick up all toast notifications
|
||||||
|
// waiting in the store and display them
|
||||||
|
toast.showNotificationForViews([VIEWS.WORKFLOW, VIEWS.NEW_WORKFLOW]);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isLoading.value = false;
|
||||||
|
canvasStore.stopLoading();
|
||||||
|
});
|
||||||
|
|
||||||
|
void usersStore.showPersonalizationSurvey();
|
||||||
|
|
||||||
checkIfRouteIsAllowed();
|
checkIfRouteIsAllowed();
|
||||||
});
|
});
|
||||||
@@ -1140,21 +1125,29 @@ onMounted(async () => {
|
|||||||
addPostMessageEventBindings();
|
addPostMessageEventBindings();
|
||||||
addKeyboardEventBindings();
|
addKeyboardEventBindings();
|
||||||
addSourceControlEventBindings();
|
addSourceControlEventBindings();
|
||||||
|
addImportEventBindings();
|
||||||
|
|
||||||
registerCustomActions();
|
registerCustomActions();
|
||||||
|
|
||||||
|
// @TODO Implement this
|
||||||
|
// this.clipboard.onPaste.value = this.onClipboardPasteEvent;
|
||||||
|
|
||||||
|
// @TODO: This currently breaks since front-end hooks are still not updated to work with pinia store
|
||||||
|
void externalHooks.run('nodeView.mount').catch(() => {});
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
removeKeyboardEventBindings();
|
|
||||||
removePostMessageEventBindings();
|
|
||||||
removeUndoRedoEventBindings();
|
removeUndoRedoEventBindings();
|
||||||
|
removePostMessageEventBindings();
|
||||||
|
removeKeyboardEventBindings();
|
||||||
removeSourceControlEventBindings();
|
removeSourceControlEventBindings();
|
||||||
|
removeImportEventBindings();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<WorkflowCanvas
|
<WorkflowCanvas
|
||||||
v-if="editableWorkflow && editableWorkflowObject"
|
v-if="editableWorkflow && editableWorkflowObject && !isLoading"
|
||||||
:workflow="editableWorkflow"
|
:workflow="editableWorkflow"
|
||||||
:workflow-object="editableWorkflowObject"
|
:workflow-object="editableWorkflowObject"
|
||||||
:fallback-nodes="fallbackNodes"
|
:fallback-nodes="fallbackNodes"
|
||||||
|
|||||||
Reference in New Issue
Block a user