mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08: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:
parent
1f420e0bd6
commit
45affe5d89
|
@ -9,45 +9,45 @@ import type {
|
|||
ROLE,
|
||||
} from '@/constants';
|
||||
import type { IMenuItem, NodeCreatorTag } from 'n8n-design-system';
|
||||
import {
|
||||
type GenericValue,
|
||||
type IConnections,
|
||||
type ICredentialsDecrypted,
|
||||
type ICredentialsEncrypted,
|
||||
type ICredentialType,
|
||||
type IDataObject,
|
||||
type INode,
|
||||
type INodeIssues,
|
||||
type INodeParameters,
|
||||
type INodeTypeDescription,
|
||||
type IPinData,
|
||||
type IRunExecutionData,
|
||||
type IRun,
|
||||
type IRunData,
|
||||
type ITaskData,
|
||||
type IWorkflowSettings as IWorkflowSettingsWorkflow,
|
||||
type WorkflowExecuteMode,
|
||||
type PublicInstalledPackage,
|
||||
type INodeTypeNameVersion,
|
||||
type ILoadOptions,
|
||||
type INodeCredentials,
|
||||
type INodeListSearchItems,
|
||||
type NodeParameterValueType,
|
||||
type IDisplayOptions,
|
||||
type ExecutionSummary,
|
||||
type FeatureFlags,
|
||||
type ExecutionStatus,
|
||||
type ITelemetryTrackProperties,
|
||||
type IUserManagementSettings,
|
||||
type WorkflowSettings,
|
||||
type IUserSettings,
|
||||
type IN8nUISettings,
|
||||
type BannerName,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
type NodeConnectionType,
|
||||
type INodeCredentialsDetails,
|
||||
type StartNodeData,
|
||||
import type {
|
||||
GenericValue,
|
||||
IConnections,
|
||||
ICredentialsDecrypted,
|
||||
ICredentialsEncrypted,
|
||||
ICredentialType,
|
||||
IDataObject,
|
||||
INode,
|
||||
INodeIssues,
|
||||
INodeParameters,
|
||||
INodeTypeDescription,
|
||||
IPinData,
|
||||
IRunExecutionData,
|
||||
IRun,
|
||||
IRunData,
|
||||
ITaskData,
|
||||
IWorkflowSettings as IWorkflowSettingsWorkflow,
|
||||
WorkflowExecuteMode,
|
||||
PublicInstalledPackage,
|
||||
INodeTypeNameVersion,
|
||||
ILoadOptions,
|
||||
INodeCredentials,
|
||||
INodeListSearchItems,
|
||||
NodeParameterValueType,
|
||||
IDisplayOptions,
|
||||
ExecutionSummary,
|
||||
FeatureFlags,
|
||||
ExecutionStatus,
|
||||
ITelemetryTrackProperties,
|
||||
IUserManagementSettings,
|
||||
WorkflowSettings,
|
||||
IUserSettings,
|
||||
IN8nUISettings,
|
||||
BannerName,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
NodeConnectionType,
|
||||
INodeCredentialsDetails,
|
||||
StartNodeData,
|
||||
} from 'n8n-workflow';
|
||||
import type { BulkCommand, Undoable } from '@/models/history';
|
||||
import type { PartialBy, TupleToUnion } from '@/utils/typeHelpers';
|
||||
|
@ -1805,9 +1805,7 @@ export type AddedNode = {
|
|||
type: string;
|
||||
openDetail?: boolean;
|
||||
isAutoAdd?: boolean;
|
||||
name?: string;
|
||||
position?: XYPosition;
|
||||
};
|
||||
} & Partial<INodeUi>;
|
||||
|
||||
export type AddedNodeConnection = {
|
||||
from: { nodeIndex: number; outputIndex?: number };
|
||||
|
|
|
@ -48,10 +48,18 @@ const props = withDefaults(
|
|||
},
|
||||
);
|
||||
|
||||
const { getSelectedEdges, getSelectedNodes, viewportRef, fitView, project } = useVueFlow({
|
||||
id: props.id,
|
||||
const { getSelectedEdges, getSelectedNodes, viewportRef, fitView, project, onPaneReady } =
|
||||
useVueFlow({
|
||||
id: props.id,
|
||||
});
|
||||
|
||||
onPaneReady(async () => {
|
||||
await onFitView();
|
||||
paneReady.value = true;
|
||||
});
|
||||
|
||||
const paneReady = ref(false);
|
||||
|
||||
/**
|
||||
* Nodes
|
||||
*/
|
||||
|
@ -183,7 +191,7 @@ function onClickPane(event: MouseEvent) {
|
|||
}
|
||||
|
||||
async function onFitView() {
|
||||
await fitView();
|
||||
await fitView({ maxZoom: 1.2, padding: 0.1 });
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -207,12 +215,12 @@ onUnmounted(() => {
|
|||
:nodes="nodes"
|
||||
:edges="connections"
|
||||
:apply-changes="false"
|
||||
fit-view-on-init
|
||||
pan-on-scroll
|
||||
snap-to-grid
|
||||
:snap-grid="[16, 16]"
|
||||
:min-zoom="0.2"
|
||||
:max-zoom="2"
|
||||
:max-zoom="4"
|
||||
:class="[$style.canvas, { [$style.visible]: paneReady }]"
|
||||
data-test-id="canvas"
|
||||
@node-drag-stop="onNodeDragStop"
|
||||
@selection-drag-stop="onSelectionDragStop"
|
||||
|
@ -253,10 +261,21 @@ onUnmounted(() => {
|
|||
data-test-id="canvas-controls"
|
||||
:class="$style.canvasControls"
|
||||
:position="controlsPosition"
|
||||
@fit-view="onFitView"
|
||||
></Controls>
|
||||
</VueFlow>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.canvas {
|
||||
opacity: 0;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.vue-flow__controls {
|
||||
display: flex;
|
||||
|
|
|
@ -61,15 +61,114 @@ describe('useCanvasOperations', () => {
|
|||
const workflow = mock<IWorkflowDb>({
|
||||
id: workflowId,
|
||||
nodes: [],
|
||||
connections: {},
|
||||
tags: [],
|
||||
usedCredentials: [],
|
||||
});
|
||||
workflowsStore.workflowsById[workflowId] = workflow;
|
||||
|
||||
workflowsStore.resetWorkflow();
|
||||
workflowsStore.resetState();
|
||||
await workflowHelpers.initState(workflow);
|
||||
|
||||
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', () => {
|
||||
it('should update node position', () => {
|
||||
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', () => {
|
||||
it('should add nodes at specified positions', async () => {
|
||||
const nodeTypeName = 'type';
|
||||
|
@ -489,6 +490,7 @@ describe('useCanvasOperations', () => {
|
|||
const nodes = [
|
||||
mockNode({ id: 'a', name: 'Node A', 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([
|
||||
|
@ -504,14 +506,27 @@ describe('useCanvasOperations', () => {
|
|||
.mockReturnValueOnce(nodes[1]);
|
||||
|
||||
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');
|
||||
|
||||
await canvasOperations.addConnections(connections, { offsetIndex });
|
||||
await canvasOperations.addConnections(connections);
|
||||
|
||||
expect(addConnectionSpy).toHaveBeenCalledWith({
|
||||
connection: [
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1261,6 +1261,7 @@ export function useNodeHelpers() {
|
|||
deleteJSPlumbConnection,
|
||||
loadNodesProperties,
|
||||
addNodes,
|
||||
addConnections,
|
||||
addConnection,
|
||||
removeConnection,
|
||||
removeConnectionByConnectionInfo,
|
||||
|
|
|
@ -65,6 +65,7 @@ import type { useRouter } from 'vue-router';
|
|||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { useTagsStore } from '@/stores/tags.store';
|
||||
import useWorkflowsEEStore from '@/stores/workflows.ee.store';
|
||||
|
||||
export function resolveParameter<T = IDataObject>(
|
||||
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
|
||||
|
@ -439,6 +440,7 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
|||
const rootStore = useRootStore();
|
||||
const templatesStore = useTemplatesStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsEEStore = useWorkflowsEEStore();
|
||||
const uiStore = useUIStore();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
@ -1063,6 +1065,17 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
|||
workflowsStore.setWorkflowVersionId(workflowData.versionId);
|
||||
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 tagIds = tags.map((tag) => tag.id);
|
||||
workflowsStore.setWorkflowTagIds(tagIds || []);
|
||||
|
|
|
@ -203,8 +203,10 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
|
|||
// canvasStore.newNodeInsertPosition = null;
|
||||
|
||||
if (isVueFlowConnection(connection)) {
|
||||
uiStore.lastSelectedNodeConnection = connection;
|
||||
uiStore.lastInteractedWithNodeConnection = connection;
|
||||
}
|
||||
uiStore.lastInteractedWithNodeHandle = connection.sourceHandle ?? null;
|
||||
uiStore.lastInteractedWithNodeId = sourceNode.id;
|
||||
|
||||
openNodeCreator({
|
||||
source: eventSource,
|
||||
|
|
|
@ -178,7 +178,6 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
const lastSelectedNode = ref<string | null>(null);
|
||||
const lastSelectedNodeOutputIndex = ref<number | null>(null);
|
||||
const lastSelectedNodeEndpointUuid = ref<string | null>(null);
|
||||
const lastSelectedNodeConnection = ref<Connection | null>(null);
|
||||
const nodeViewOffsetPosition = ref<[number, number]>([0, 0]);
|
||||
const nodeViewMoveInProgress = ref<boolean>(false);
|
||||
const selectedNodes = ref<INodeUi[]>([]);
|
||||
|
@ -189,6 +188,11 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
const pendingNotificationsForViews = ref<{ [key in VIEWS]?: NotificationOptions[] }>({});
|
||||
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 workflowsStore = useWorkflowsStore();
|
||||
const rootStore = useRootStore();
|
||||
|
@ -275,6 +279,14 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
return null;
|
||||
});
|
||||
|
||||
const lastInteractedWithNode = computed(() => {
|
||||
if (lastInteractedWithNodeId.value) {
|
||||
return workflowsStore.getNodeById(lastInteractedWithNodeId.value);
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const isVersionsOpen = computed(() => {
|
||||
return modalsById.value[VERSIONS_MODAL_KEY].open;
|
||||
});
|
||||
|
@ -600,6 +612,12 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
delete pendingNotificationsForViews.value[view];
|
||||
};
|
||||
|
||||
function resetLastInteractedWith() {
|
||||
lastInteractedWithNodeConnection.value = null;
|
||||
lastInteractedWithNodeHandle.value = null;
|
||||
lastInteractedWithNodeId.value = null;
|
||||
}
|
||||
|
||||
return {
|
||||
appliedTheme,
|
||||
logo,
|
||||
|
@ -621,7 +639,10 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
selectedNodes,
|
||||
bannersHeight,
|
||||
lastSelectedNodeEndpointUuid,
|
||||
lastSelectedNodeConnection,
|
||||
lastInteractedWithNodeConnection,
|
||||
lastInteractedWithNodeHandle,
|
||||
lastInteractedWithNodeId,
|
||||
lastInteractedWithNode,
|
||||
nodeViewOffsetPosition,
|
||||
nodeViewMoveInProgress,
|
||||
nodeViewInitialized,
|
||||
|
@ -670,6 +691,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
|
|||
clearBannerStack,
|
||||
setNotificationsForView,
|
||||
deleteNotificationsForView,
|
||||
resetLastInteractedWith,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@ import { i18n } from '@/plugins/i18n';
|
|||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import type { ProjectSharingData } from '@/types/projects.types';
|
||||
|
||||
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
|
||||
name: '',
|
||||
|
@ -452,6 +453,15 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||
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() {
|
||||
workflow.value = createEmptyWorkflow();
|
||||
}
|
||||
|
@ -1589,6 +1599,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||
fetchAllWorkflows,
|
||||
fetchWorkflow,
|
||||
getNewWorkflowData,
|
||||
makeNewWorkflowShareable,
|
||||
resetWorkflow,
|
||||
resetState,
|
||||
addExecutingNode,
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
import type {
|
||||
ConnectionTypes,
|
||||
ExecutionStatus,
|
||||
IConnection,
|
||||
INodeConnections,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui';
|
||||
import type { DefaultEdge, Node, NodeProps, Position } from '@vue-flow/core';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type { ComputedRef, Ref } from 'vue';
|
||||
import type { PartialBy } from '@/utils/typeHelpers';
|
||||
|
||||
export type CanvasConnectionPortType = ConnectionTypes;
|
||||
|
||||
|
@ -107,13 +108,14 @@ export interface CanvasConnectionData {
|
|||
|
||||
export type CanvasConnection = DefaultEdge<CanvasConnectionData>;
|
||||
|
||||
export interface CanvasPluginContext {
|
||||
instance: BrowserJsPlumbInstance;
|
||||
}
|
||||
|
||||
export interface CanvasPlugin {
|
||||
(ctx: CanvasPluginContext): void;
|
||||
}
|
||||
export type CanvasConnectionCreateData = {
|
||||
source: string;
|
||||
target: string;
|
||||
data: {
|
||||
source: PartialBy<IConnection, 'node'>;
|
||||
target: PartialBy<IConnection, 'node'>;
|
||||
};
|
||||
};
|
||||
|
||||
export interface CanvasNodeInjectionData {
|
||||
id: Ref<string>;
|
||||
|
|
|
@ -6,8 +6,6 @@ import type { Connection } from '@vue-flow/core';
|
|||
import { v4 as uuid } from 'uuid';
|
||||
import { isValidCanvasConnectionMode, isValidNodeConnectionType } from '@/utils/typeGuards';
|
||||
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(
|
||||
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({
|
||||
mode,
|
||||
type = NodeConnectionType.Main,
|
||||
|
|
|
@ -28,10 +28,9 @@ import type {
|
|||
XYPosition,
|
||||
} from '@/Interface';
|
||||
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 {
|
||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
EnterpriseEditionFeature,
|
||||
MAIN_HEADER_TABS,
|
||||
|
@ -46,13 +45,8 @@ import {
|
|||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { TelemetryHelpers } from 'n8n-workflow';
|
||||
import type {
|
||||
NodeConnectionType,
|
||||
ExecutionSummary,
|
||||
IConnection,
|
||||
IWorkflowBase,
|
||||
} from 'n8n-workflow';
|
||||
import { TelemetryHelpers, NodeConnectionType } from 'n8n-workflow';
|
||||
import type { IDataObject, ExecutionSummary, IConnection, IWorkflowBase } from 'n8n-workflow';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||
|
@ -70,11 +64,8 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
|||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useHistoryStore } from '@/stores/history.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 { useExecutionDebugging } from '@/composables/useExecutionDebugging';
|
||||
import type { ProjectSharingData } from '@/types/projects.types';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { sourceControlEventBus } from '@/event-bus/source-control';
|
||||
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 CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue';
|
||||
import { nodeViewEventBus } from '@/event-bus';
|
||||
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
||||
import { tryToParseNumber } from '@/utils/typesUtils';
|
||||
import { useTemplatesStore } from '@/stores/templates.store';
|
||||
import { createEventBus } from 'n8n-design-system';
|
||||
|
@ -105,15 +97,13 @@ const telemetry = useTelemetry();
|
|||
const externalHooks = useExternalHooks();
|
||||
const toast = useToast();
|
||||
const message = useMessage();
|
||||
const titleChange = useTitleChange();
|
||||
const { titleReset, titleSet } = useTitleChange();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const posthog = usePostHog();
|
||||
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const uiStore = useUIStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const workflowsEEStore = useWorkflowsEEStore();
|
||||
const sourceControlStore = useSourceControlStore();
|
||||
const nodeCreatorStore = useNodeCreatorStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
@ -153,6 +143,10 @@ const {
|
|||
revertDeleteConnection,
|
||||
setNodeActiveByName,
|
||||
addConnections,
|
||||
importWorkflowData,
|
||||
fetchWorkflowDataFromUrl,
|
||||
resetWorkspace,
|
||||
initializeWorkspace,
|
||||
editableWorkflow,
|
||||
editableWorkflowObject,
|
||||
} = useCanvasOperations({ router, lastClickPosition });
|
||||
|
@ -202,12 +196,6 @@ const fallbackNodes = computed<INodeUi[]>(() =>
|
|||
*/
|
||||
|
||||
async function initializeData() {
|
||||
isLoading.value = true;
|
||||
canvasStore.startLoading();
|
||||
|
||||
resetWorkspace();
|
||||
titleChange.titleReset();
|
||||
|
||||
const loadPromises = (() => {
|
||||
if (settingsStore.isPreviewMode && isDemoRoute.value) return [];
|
||||
|
||||
|
@ -232,9 +220,6 @@ async function initializeData() {
|
|||
return promises;
|
||||
})();
|
||||
|
||||
// @TODO Implement this
|
||||
// this.clipboard.onPaste.value = this.onClipboardPasteEvent;
|
||||
|
||||
try {
|
||||
await Promise.all(loadPromises);
|
||||
} catch (error) {
|
||||
|
@ -244,23 +229,10 @@ async function initializeData() {
|
|||
i18n.baseText('nodeView.showError.mounted1.message') + ':',
|
||||
);
|
||||
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
|
||||
// as only the route changed but all the needed data is already loaded
|
||||
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 (!workflowId.value || isNewWorkflowRoute.value) {
|
||||
if (route.meta?.nodeView === true) {
|
||||
await initializeViewForNewWorkflow();
|
||||
await initializeWorkspaceForNewWorkflow();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Load workflow data
|
||||
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,
|
||||
});
|
||||
}
|
||||
await initializeWorkspaceForExistingWorkflow(workflowId.value);
|
||||
}
|
||||
|
||||
nodeHelpers.updateNodesInputIssues();
|
||||
|
@ -311,67 +268,40 @@ async function initializeView() {
|
|||
nodeHelpers.updateNodesParameterIssues();
|
||||
|
||||
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();
|
||||
|
||||
await workflowsStore.getNewWorkflowData(undefined, projectsStore.currentProjectId);
|
||||
workflowsStore.makeNewWorkflowShareable();
|
||||
|
||||
workflowsStore.currentWorkflowExecutions = [];
|
||||
executionsStore.activeExecution = null;
|
||||
uiStore.stateIsDirty = false;
|
||||
uiStore.nodeViewInitialized = true;
|
||||
executionsStore.activeExecution = null;
|
||||
|
||||
makeNewWorkflowShareable();
|
||||
await runAutoAddManualTriggerExperiment();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-populate the canvas with the manual trigger node
|
||||
* if the experiment is enabled and the user is in the variant group
|
||||
*/
|
||||
async function runAutoAddManualTriggerExperiment() {
|
||||
if (
|
||||
posthog.getVariant(CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name) !==
|
||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.variant
|
||||
) {
|
||||
return;
|
||||
async function initializeWorkspaceForExistingWorkflow(id: string) {
|
||||
resetWorkspace();
|
||||
|
||||
try {
|
||||
const workflowData = await workflowsStore.fetchWorkflow(id);
|
||||
|
||||
await openWorkflow(workflowData);
|
||||
await initializeDebugMode();
|
||||
|
||||
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) {
|
||||
const selectedExecution = executionsStore.activeExecution;
|
||||
|
||||
resetWorkspace();
|
||||
titleSet(workflow.value.name, 'IDLE');
|
||||
|
||||
await workflowHelpers.initState(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;
|
||||
}
|
||||
await initializeWorkspace(data);
|
||||
|
||||
void externalHooks.run('workflow.open', {
|
||||
workflowId: data.id,
|
||||
workflowName: data.name,
|
||||
});
|
||||
|
||||
if (selectedExecution?.workflowId !== data.id) {
|
||||
executionsStore.activeExecution = null;
|
||||
workflowsStore.currentWorkflowExecutions = [];
|
||||
} else {
|
||||
executionsStore.activeExecution = selectedExecution;
|
||||
}
|
||||
// @TODO Check why this is needed when working on executions
|
||||
// const selectedExecution = executionsStore.activeExecution;
|
||||
// if (selectedExecution?.workflowId !== data.id) {
|
||||
// executionsStore.activeExecution = null;
|
||||
// workflowsStore.currentWorkflowExecutions = [];
|
||||
// } else {
|
||||
// executionsStore.activeExecution = selectedExecution;
|
||||
// }
|
||||
|
||||
await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflow.value.homeProject);
|
||||
fitView();
|
||||
}
|
||||
|
||||
function trackOpenWorkflowFromOnboardingTemplate() {
|
||||
if (workflow.value.meta?.onboardingId) {
|
||||
telemetry.track(
|
||||
`User opened workflow from onboarding template with ID ${workflow.value.meta.onboardingId}`,
|
||||
{
|
||||
workflow_id: workflowId.value,
|
||||
},
|
||||
{
|
||||
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;
|
||||
telemetry.track(
|
||||
`User opened workflow from onboarding template with ID ${workflow.value.meta?.onboardingId}`,
|
||||
{
|
||||
workflow_id: workflowId.value,
|
||||
},
|
||||
{
|
||||
withPostHog: true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -485,7 +388,6 @@ async function openWorkflowTemplate(templateId: string) {
|
|||
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
canvasEventBus.emit('fitView');
|
||||
canvasStore.stopLoading();
|
||||
|
||||
void externalHooks.run('template.open', {
|
||||
|
@ -493,6 +395,8 @@ async function openWorkflowTemplate(templateId: string) {
|
|||
templateName: data.name,
|
||||
workflow: data.workflow,
|
||||
});
|
||||
|
||||
fitView();
|
||||
}
|
||||
|
||||
function trackOpenWorkflowTemplate(templateId: string) {
|
||||
|
@ -631,8 +535,46 @@ function onRevertDeleteConnection({ connection }: { connection: [IConnection, IC
|
|||
* Import / Export
|
||||
*/
|
||||
|
||||
async function importWorkflowExact(_workflow: IWorkflowDataUpdate) {
|
||||
// @TODO
|
||||
async function importWorkflowExact({ workflow: workflowData }: { workflow: IWorkflowDataUpdate }) {
|
||||
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 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) {
|
||||
|
@ -865,7 +827,7 @@ async function onSourceControlPull() {
|
|||
if (workflowId.value !== null && !uiStore.stateIsDirty) {
|
||||
const workflowData = await workflowsStore.fetchWorkflow(workflowId.value);
|
||||
if (workflowData) {
|
||||
titleChange.titleSet(workflowData.name, 'IDLE');
|
||||
titleSet(workflowData.name, 'IDLE');
|
||||
await openWorkflow(workflowData);
|
||||
}
|
||||
}
|
||||
|
@ -909,7 +871,7 @@ async function onPostMessageReceived(message: MessageEvent) {
|
|||
const json = JSON.parse(message.data);
|
||||
if (json && json.command === 'openWorkflow') {
|
||||
try {
|
||||
await importWorkflowExact(json.data);
|
||||
await importWorkflowExact(json);
|
||||
canOpenNDV.value = json.canOpenNDV ?? true;
|
||||
hideNodeIssues.value = json.hideNodeIssues ?? false;
|
||||
isExecutionPreview.value = false;
|
||||
|
@ -1009,9 +971,9 @@ function checkIfRouteIsAllowed() {
|
|||
* Debug mode
|
||||
*/
|
||||
|
||||
async function checkAndInitDebugMode() {
|
||||
async function initializeDebugMode() {
|
||||
if (route.name === VIEWS.EXECUTION_DEBUG) {
|
||||
titleChange.titleSet(workflowsStore.workflowName, 'DEBUG');
|
||||
titleSet(workflowsStore.workflowName, 'DEBUG');
|
||||
if (!workflowsStore.isInDebugMode) {
|
||||
await applyExecutionData(route.params.executionId as string);
|
||||
workflowsStore.isInDebugMode = true;
|
||||
|
@ -1019,6 +981,14 @@ async function checkAndInitDebugMode() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Canvas
|
||||
*/
|
||||
|
||||
function fitView() {
|
||||
setTimeout(() => canvasEventBus.emit('fitView'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse events
|
||||
*/
|
||||
|
@ -1130,8 +1100,23 @@ onBeforeMount(() => {
|
|||
});
|
||||
|
||||
onMounted(async () => {
|
||||
canvasStore.startLoading();
|
||||
titleReset();
|
||||
resetWorkspace();
|
||||
|
||||
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();
|
||||
});
|
||||
|
@ -1140,21 +1125,29 @@ onMounted(async () => {
|
|||
addPostMessageEventBindings();
|
||||
addKeyboardEventBindings();
|
||||
addSourceControlEventBindings();
|
||||
addImportEventBindings();
|
||||
|
||||
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(() => {
|
||||
removeKeyboardEventBindings();
|
||||
removePostMessageEventBindings();
|
||||
removeUndoRedoEventBindings();
|
||||
removePostMessageEventBindings();
|
||||
removeKeyboardEventBindings();
|
||||
removeSourceControlEventBindings();
|
||||
removeImportEventBindings();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WorkflowCanvas
|
||||
v-if="editableWorkflow && editableWorkflowObject"
|
||||
v-if="editableWorkflow && editableWorkflowObject && !isLoading"
|
||||
:workflow="editableWorkflow"
|
||||
:workflow-object="editableWorkflowObject"
|
||||
:fallback-nodes="fallbackNodes"
|
||||
|
|
Loading…
Reference in a new issue