2024-05-23 01:42:10 -07:00
|
|
|
<script setup lang="ts">
|
2024-07-08 03:25:18 -07:00
|
|
|
import {
|
|
|
|
computed,
|
|
|
|
defineAsyncComponent,
|
|
|
|
nextTick,
|
|
|
|
onBeforeMount,
|
|
|
|
onBeforeUnmount,
|
|
|
|
onMounted,
|
|
|
|
ref,
|
|
|
|
useCssModule,
|
2024-08-13 13:14:06 -07:00
|
|
|
watch,
|
2024-07-08 03:25:18 -07:00
|
|
|
} from 'vue';
|
2024-07-25 08:44:26 -07:00
|
|
|
import { useRoute, useRouter } from 'vue-router';
|
2024-05-23 01:42:10 -07:00
|
|
|
import WorkflowCanvas from '@/components/canvas/WorkflowCanvas.vue';
|
|
|
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
|
|
import { useUIStore } from '@/stores/ui.store';
|
2024-07-11 03:03:46 -07:00
|
|
|
import CanvasRunWorkflowButton from '@/components/canvas/elements/buttons/CanvasRunWorkflowButton.vue';
|
2024-05-23 01:42:10 -07:00
|
|
|
import { useI18n } from '@/composables/useI18n';
|
|
|
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
|
|
import { useRunWorkflow } from '@/composables/useRunWorkflow';
|
|
|
|
import type {
|
|
|
|
AddedNodesAndConnections,
|
2024-07-23 01:27:09 -07:00
|
|
|
IExecutionResponse,
|
2024-05-23 01:42:10 -07:00
|
|
|
INodeUi,
|
2024-06-17 05:46:55 -07:00
|
|
|
IUpdateInformation,
|
|
|
|
IWorkflowDataUpdate,
|
2024-07-08 03:25:18 -07:00
|
|
|
IWorkflowDb,
|
2024-07-11 08:29:06 -07:00
|
|
|
IWorkflowTemplate,
|
2024-07-18 04:00:54 -07:00
|
|
|
NodeCreatorOpenSource,
|
2024-05-23 01:42:10 -07:00
|
|
|
ToggleNodeCreatorOptions,
|
|
|
|
XYPosition,
|
|
|
|
} from '@/Interface';
|
2024-07-23 01:28:52 -07:00
|
|
|
import type { Connection, ViewportTransform } from '@vue-flow/core';
|
2024-07-23 00:16:56 -07:00
|
|
|
import type {
|
|
|
|
CanvasConnectionCreateData,
|
|
|
|
CanvasNode,
|
|
|
|
CanvasNodeMoveEvent,
|
|
|
|
ConnectStartEvent,
|
|
|
|
} from '@/types';
|
2024-07-11 03:03:46 -07:00
|
|
|
import { CanvasNodeRenderType } from '@/types';
|
2024-06-25 02:11:44 -07:00
|
|
|
import {
|
2024-07-11 03:03:46 -07:00
|
|
|
CHAT_TRIGGER_NODE_TYPE,
|
2024-06-25 02:11:44 -07:00
|
|
|
EnterpriseEditionFeature,
|
2024-07-08 05:57:42 -07:00
|
|
|
MAIN_HEADER_TABS,
|
2024-07-11 03:03:46 -07:00
|
|
|
MANUAL_CHAT_TRIGGER_NODE_TYPE,
|
2024-06-25 02:11:44 -07:00
|
|
|
MODAL_CONFIRM,
|
2024-07-10 02:22:31 -07:00
|
|
|
NODE_CREATOR_OPEN_SOURCES,
|
2024-07-11 03:03:46 -07:00
|
|
|
START_NODE_TYPE,
|
2024-07-18 04:00:54 -07:00
|
|
|
STICKY_NODE_TYPE,
|
2024-07-19 05:49:52 -07:00
|
|
|
VALID_WORKFLOW_IMPORT_URL_REGEX,
|
2024-06-25 02:11:44 -07:00
|
|
|
VIEWS,
|
2024-07-24 02:10:02 -07:00
|
|
|
WORKFLOW_LM_CHAT_MODAL_KEY,
|
2024-06-25 02:11:44 -07:00
|
|
|
} from '@/constants';
|
2024-05-23 01:42:10 -07:00
|
|
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
|
|
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
|
|
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
2024-07-19 05:49:52 -07:00
|
|
|
import { TelemetryHelpers, NodeConnectionType, jsonParse } from 'n8n-workflow';
|
2024-07-18 01:59:11 -07:00
|
|
|
import type { IDataObject, ExecutionSummary, IConnection, IWorkflowBase } from 'n8n-workflow';
|
2024-05-23 01:42:10 -07:00
|
|
|
import { useToast } from '@/composables/useToast';
|
|
|
|
import { useSettingsStore } from '@/stores/settings.store';
|
|
|
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
|
|
|
import useEnvironmentsStore from '@/stores/environments.ee.store';
|
|
|
|
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
2024-06-18 10:15:12 -07:00
|
|
|
import { useRootStore } from '@/stores/root.store';
|
2024-06-04 05:36:27 -07:00
|
|
|
import { historyBus } from '@/models/history';
|
|
|
|
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
2024-06-17 05:46:55 -07:00
|
|
|
import { useExecutionsStore } from '@/stores/executions.store';
|
2024-06-25 02:11:44 -07:00
|
|
|
import { useCanvasStore } from '@/stores/canvas.store';
|
|
|
|
import { useMessage } from '@/composables/useMessage';
|
|
|
|
import { useTitleChange } from '@/composables/useTitleChange';
|
|
|
|
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
|
|
|
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
|
|
|
import { useTelemetry } from '@/composables/useTelemetry';
|
|
|
|
import { useHistoryStore } from '@/stores/history.store';
|
|
|
|
import { useProjectsStore } from '@/stores/projects.store';
|
2024-07-08 03:25:18 -07:00
|
|
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
|
|
|
import { useExecutionDebugging } from '@/composables/useExecutionDebugging';
|
|
|
|
import { useUsersStore } from '@/stores/users.store';
|
|
|
|
import { sourceControlEventBus } from '@/event-bus/source-control';
|
|
|
|
import { useTagsStore } from '@/stores/tags.store';
|
|
|
|
import { usePushConnectionStore } from '@/stores/pushConnection.store';
|
2024-07-10 00:56:39 -07:00
|
|
|
import { useNDVStore } from '@/stores/ndv.store';
|
2024-07-10 01:53:27 -07:00
|
|
|
import { getNodeViewTab } from '@/utils/canvasUtils';
|
|
|
|
import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/buttons/CanvasStopCurrentExecutionButton.vue';
|
|
|
|
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
|
2024-07-11 06:37:01 -07:00
|
|
|
import CanvasClearExecutionDataButton from '@/components/canvas/elements/buttons/CanvasClearExecutionDataButton.vue';
|
2024-07-11 03:03:46 -07:00
|
|
|
import { nodeViewEventBus } from '@/event-bus';
|
2024-07-18 01:59:11 -07:00
|
|
|
import * as NodeViewUtils from '@/utils/nodeViewUtils';
|
2024-07-11 08:29:06 -07:00
|
|
|
import { tryToParseNumber } from '@/utils/typesUtils';
|
|
|
|
import { useTemplatesStore } from '@/stores/templates.store';
|
|
|
|
import { createEventBus } from 'n8n-design-system';
|
2024-07-18 04:00:54 -07:00
|
|
|
import type { PinDataSource } from '@/composables/usePinnedData';
|
2024-07-19 05:49:52 -07:00
|
|
|
import { useClipboard } from '@/composables/useClipboard';
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-23 03:30:29 -07:00
|
|
|
const LazyNodeCreation = defineAsyncComponent(
|
2024-05-23 01:42:10 -07:00
|
|
|
async () => await import('@/components/Node/NodeCreation.vue'),
|
|
|
|
);
|
|
|
|
|
2024-07-23 03:30:29 -07:00
|
|
|
const LazyNodeDetailsView = defineAsyncComponent(
|
2024-06-17 05:46:55 -07:00
|
|
|
async () => await import('@/components/NodeDetailsView.vue'),
|
|
|
|
);
|
|
|
|
|
2024-05-23 01:42:10 -07:00
|
|
|
const $style = useCssModule();
|
|
|
|
const router = useRouter();
|
|
|
|
const route = useRoute();
|
|
|
|
const i18n = useI18n();
|
2024-06-25 02:11:44 -07:00
|
|
|
const telemetry = useTelemetry();
|
2024-05-23 01:42:10 -07:00
|
|
|
const externalHooks = useExternalHooks();
|
|
|
|
const toast = useToast();
|
2024-06-25 02:11:44 -07:00
|
|
|
const message = useMessage();
|
2024-07-18 01:59:11 -07:00
|
|
|
const { titleReset, titleSet } = useTitleChange();
|
2024-06-25 02:11:44 -07:00
|
|
|
const workflowHelpers = useWorkflowHelpers({ router });
|
2024-07-08 03:25:18 -07:00
|
|
|
const nodeHelpers = useNodeHelpers();
|
2024-05-23 01:42:10 -07:00
|
|
|
|
|
|
|
const nodeTypesStore = useNodeTypesStore();
|
|
|
|
const uiStore = useUIStore();
|
|
|
|
const workflowsStore = useWorkflowsStore();
|
|
|
|
const sourceControlStore = useSourceControlStore();
|
|
|
|
const nodeCreatorStore = useNodeCreatorStore();
|
|
|
|
const settingsStore = useSettingsStore();
|
|
|
|
const credentialsStore = useCredentialsStore();
|
|
|
|
const environmentsStore = useEnvironmentsStore();
|
|
|
|
const externalSecretsStore = useExternalSecretsStore();
|
|
|
|
const rootStore = useRootStore();
|
2024-06-17 05:46:55 -07:00
|
|
|
const executionsStore = useExecutionsStore();
|
2024-06-25 02:11:44 -07:00
|
|
|
const canvasStore = useCanvasStore();
|
|
|
|
const npsSurveyStore = useNpsSurveyStore();
|
|
|
|
const historyStore = useHistoryStore();
|
|
|
|
const projectsStore = useProjectsStore();
|
2024-07-08 03:25:18 -07:00
|
|
|
const usersStore = useUsersStore();
|
|
|
|
const tagsStore = useTagsStore();
|
|
|
|
const pushConnectionStore = usePushConnectionStore();
|
2024-07-10 00:56:39 -07:00
|
|
|
const ndvStore = useNDVStore();
|
2024-07-11 08:29:06 -07:00
|
|
|
const templatesStore = useTemplatesStore();
|
|
|
|
|
|
|
|
const canvasEventBus = createEventBus();
|
2024-06-25 02:11:44 -07:00
|
|
|
|
2024-07-10 01:53:27 -07:00
|
|
|
const { runWorkflow, stopCurrentExecution, stopWaitingForWebhook } = useRunWorkflow({ router });
|
2024-06-04 05:36:27 -07:00
|
|
|
const {
|
|
|
|
updateNodePosition,
|
2024-07-23 00:16:56 -07:00
|
|
|
updateNodesPosition,
|
|
|
|
revertUpdateNodePosition,
|
2024-06-17 05:46:55 -07:00
|
|
|
renameNode,
|
|
|
|
revertRenameNode,
|
|
|
|
setNodeActive,
|
2024-06-25 02:11:44 -07:00
|
|
|
setNodeSelected,
|
2024-07-18 04:00:54 -07:00
|
|
|
toggleNodesDisabled,
|
2024-07-22 23:49:37 -07:00
|
|
|
revertToggleNodeDisabled,
|
2024-07-18 04:00:54 -07:00
|
|
|
toggleNodesPinned,
|
2024-07-15 03:00:52 -07:00
|
|
|
setNodeParameters,
|
2024-06-04 05:36:27 -07:00
|
|
|
deleteNode,
|
2024-07-18 04:00:54 -07:00
|
|
|
deleteNodes,
|
2024-07-19 05:49:52 -07:00
|
|
|
copyNodes,
|
|
|
|
cutNodes,
|
|
|
|
duplicateNodes,
|
2024-06-04 05:36:27 -07:00
|
|
|
revertDeleteNode,
|
2024-06-25 02:11:44 -07:00
|
|
|
addNodes,
|
2024-07-22 23:49:53 -07:00
|
|
|
revertAddNode,
|
2024-06-04 05:36:27 -07:00
|
|
|
createConnection,
|
2024-07-22 10:26:47 -07:00
|
|
|
revertCreateConnection,
|
2024-06-04 05:36:27 -07:00
|
|
|
deleteConnection,
|
|
|
|
revertDeleteConnection,
|
2024-06-17 05:46:55 -07:00
|
|
|
setNodeActiveByName,
|
2024-06-25 02:11:44 -07:00
|
|
|
addConnections,
|
2024-07-18 01:59:11 -07:00
|
|
|
importWorkflowData,
|
|
|
|
fetchWorkflowDataFromUrl,
|
|
|
|
resetWorkspace,
|
|
|
|
initializeWorkspace,
|
2024-06-25 02:11:44 -07:00
|
|
|
editableWorkflow,
|
|
|
|
editableWorkflowObject,
|
2024-07-25 08:44:26 -07:00
|
|
|
lastClickPosition,
|
|
|
|
} = useCanvasOperations({ router });
|
2024-07-08 03:25:18 -07:00
|
|
|
const { applyExecutionData } = useExecutionDebugging();
|
2024-07-19 05:49:52 -07:00
|
|
|
useClipboard({ onPaste: onClipboardPaste });
|
2024-05-23 01:42:10 -07:00
|
|
|
|
|
|
|
const isLoading = ref(true);
|
2024-06-25 02:11:44 -07:00
|
|
|
const isBlankRedirect = ref(false);
|
2024-05-23 01:42:10 -07:00
|
|
|
const readOnlyNotification = ref<null | { visible: boolean }>(null);
|
|
|
|
|
2024-06-17 05:46:55 -07:00
|
|
|
const isProductionExecutionPreview = ref(false);
|
|
|
|
const isExecutionPreview = ref(false);
|
|
|
|
|
|
|
|
const canOpenNDV = ref(true);
|
|
|
|
const hideNodeIssues = ref(false);
|
|
|
|
|
2024-07-08 05:57:42 -07:00
|
|
|
const workflowId = computed<string>(() => route.params.name as string);
|
2024-05-23 01:42:10 -07:00
|
|
|
const workflow = computed(() => workflowsStore.workflowsById[workflowId.value]);
|
|
|
|
|
2024-07-09 05:58:36 -07:00
|
|
|
const isNewWorkflowRoute = computed(() => route.name === VIEWS.NEW_WORKFLOW);
|
2024-05-23 01:42:10 -07:00
|
|
|
const isDemoRoute = computed(() => route.name === VIEWS.DEMO);
|
|
|
|
const isReadOnlyRoute = computed(() => route?.meta?.readOnlyCanvas === true);
|
|
|
|
const isReadOnlyEnvironment = computed(() => {
|
|
|
|
return sourceControlStore.preferences.branchReadOnly;
|
|
|
|
});
|
|
|
|
|
2024-07-11 03:03:46 -07:00
|
|
|
const isCanvasReadOnly = computed(() => {
|
2024-07-23 01:27:09 -07:00
|
|
|
return isDemoRoute.value || isReadOnlyEnvironment.value;
|
2024-07-11 03:03:46 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
const fallbackNodes = computed<INodeUi[]>(() =>
|
2024-07-23 01:27:09 -07:00
|
|
|
isLoading.value || isCanvasReadOnly.value
|
2024-07-11 03:03:46 -07:00
|
|
|
? []
|
|
|
|
: [
|
|
|
|
{
|
|
|
|
id: CanvasNodeRenderType.AddNodes,
|
|
|
|
name: CanvasNodeRenderType.AddNodes,
|
|
|
|
type: CanvasNodeRenderType.AddNodes,
|
|
|
|
typeVersion: 1,
|
|
|
|
position: [0, 0],
|
|
|
|
parameters: {},
|
|
|
|
},
|
|
|
|
],
|
|
|
|
);
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
/**
|
|
|
|
* Initialization
|
|
|
|
*/
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
async function initializeData() {
|
2024-07-08 03:25:18 -07:00
|
|
|
const loadPromises = (() => {
|
|
|
|
if (settingsStore.isPreviewMode && isDemoRoute.value) return [];
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-08 03:25:18 -07:00
|
|
|
const promises: Array<Promise<unknown>> = [
|
2024-05-23 01:42:10 -07:00
|
|
|
workflowsStore.fetchActiveWorkflows(),
|
|
|
|
credentialsStore.fetchAllCredentials(),
|
|
|
|
credentialsStore.fetchCredentialTypes(true),
|
2024-07-08 03:25:18 -07:00
|
|
|
];
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-19 05:35:36 -07:00
|
|
|
if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.Variables]) {
|
2024-07-08 03:25:18 -07:00
|
|
|
promises.push(environmentsStore.fetchAllVariables());
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
|
|
|
|
2024-07-19 05:35:36 -07:00
|
|
|
if (settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.ExternalSecrets]) {
|
2024-07-08 03:25:18 -07:00
|
|
|
promises.push(externalSecretsStore.fetchAllSecrets());
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
2024-07-08 03:25:18 -07:00
|
|
|
|
|
|
|
if (nodeTypesStore.allNodeTypes.length === 0) {
|
|
|
|
promises.push(nodeTypesStore.getNodeTypes());
|
|
|
|
}
|
|
|
|
|
|
|
|
return promises;
|
|
|
|
})();
|
|
|
|
|
2024-05-23 01:42:10 -07:00
|
|
|
try {
|
|
|
|
await Promise.all(loadPromises);
|
|
|
|
} catch (error) {
|
2024-07-08 03:25:18 -07:00
|
|
|
toast.showError(
|
2024-05-23 01:42:10 -07:00
|
|
|
error,
|
|
|
|
i18n.baseText('nodeView.showError.mounted1.title'),
|
|
|
|
i18n.baseText('nodeView.showError.mounted1.message') + ':',
|
|
|
|
);
|
2024-07-08 03:25:18 -07:00
|
|
|
return;
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
async function initializeRoute() {
|
2024-06-25 02:11:44 -07:00
|
|
|
// 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') {
|
|
|
|
uiStore.stateIsDirty = false;
|
|
|
|
return;
|
|
|
|
}
|
2024-06-04 05:36:27 -07:00
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
// This function is called on route change as well, so we need to do the following:
|
|
|
|
// - if the redirect is blank, then do nothing
|
|
|
|
// - if the route is the template import view, then open the template
|
|
|
|
// - if the user is leaving the current view without saving the changes, then show a confirmation modal
|
|
|
|
if (isBlankRedirect.value) {
|
|
|
|
isBlankRedirect.value = false;
|
|
|
|
} else if (route.name === VIEWS.TEMPLATE_IMPORT) {
|
2024-07-11 08:29:06 -07:00
|
|
|
const templateId = route.params.id;
|
|
|
|
await openWorkflowTemplate(templateId.toString());
|
2024-06-25 02:11:44 -07:00
|
|
|
} else {
|
|
|
|
historyStore.reset();
|
|
|
|
|
|
|
|
// If there is no workflow id, treat it as a new workflow
|
2024-07-08 05:57:42 -07:00
|
|
|
if (!workflowId.value || isNewWorkflowRoute.value) {
|
2024-06-25 02:11:44 -07:00
|
|
|
if (route.meta?.nodeView === true) {
|
2024-07-18 01:59:11 -07:00
|
|
|
await initializeWorkspaceForNewWorkflow();
|
2024-06-25 02:11:44 -07:00
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
await initializeWorkspaceForExistingWorkflow(workflowId.value);
|
2024-06-25 02:11:44 -07:00
|
|
|
}
|
|
|
|
|
2024-07-08 03:25:18 -07:00
|
|
|
nodeHelpers.updateNodesInputIssues();
|
|
|
|
nodeHelpers.updateNodesCredentialsIssues();
|
|
|
|
nodeHelpers.updateNodesParameterIssues();
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
await loadCredentials();
|
2024-08-13 13:14:06 -07:00
|
|
|
await initializeDebugMode();
|
2024-06-17 05:46:55 -07:00
|
|
|
}
|
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
async function initializeWorkspaceForNewWorkflow() {
|
2024-06-25 02:11:44 -07:00
|
|
|
resetWorkspace();
|
|
|
|
|
|
|
|
await workflowsStore.getNewWorkflowData(undefined, projectsStore.currentProjectId);
|
2024-07-18 01:59:11 -07:00
|
|
|
workflowsStore.makeNewWorkflowShareable();
|
2024-06-25 02:11:44 -07:00
|
|
|
|
|
|
|
uiStore.nodeViewInitialized = true;
|
2024-07-08 03:25:18 -07:00
|
|
|
}
|
2024-06-25 02:11:44 -07:00
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
async function initializeWorkspaceForExistingWorkflow(id: string) {
|
|
|
|
resetWorkspace();
|
2024-07-08 03:25:18 -07:00
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
try {
|
|
|
|
const workflowData = await workflowsStore.fetchWorkflow(id);
|
2024-07-08 03:25:18 -07:00
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
await openWorkflow(workflowData);
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
if (workflowData.meta?.onboardingId) {
|
|
|
|
trackOpenWorkflowFromOnboardingTemplate();
|
|
|
|
}
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
await projectsStore.setProjectNavActiveIdByWorkflowHomeProject(workflow.value.homeProject);
|
|
|
|
} catch (error) {
|
|
|
|
toast.showError(error, i18n.baseText('openWorkflow.workflowNotFoundError'));
|
2024-06-25 02:11:44 -07:00
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
void router.push({
|
|
|
|
name: VIEWS.NEW_WORKFLOW,
|
|
|
|
});
|
|
|
|
} finally {
|
|
|
|
uiStore.nodeViewInitialized = true;
|
|
|
|
}
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
|
|
|
|
2024-07-08 03:25:18 -07:00
|
|
|
/**
|
|
|
|
* Workflow
|
|
|
|
*/
|
|
|
|
|
|
|
|
async function openWorkflow(data: IWorkflowDb) {
|
|
|
|
resetWorkspace();
|
2024-07-18 01:59:11 -07:00
|
|
|
titleSet(workflow.value.name, 'IDLE');
|
2024-07-08 03:25:18 -07:00
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
await initializeWorkspace(data);
|
2024-07-08 03:25:18 -07:00
|
|
|
|
|
|
|
void externalHooks.run('workflow.open', {
|
|
|
|
workflowId: data.id,
|
|
|
|
workflowName: data.name,
|
|
|
|
});
|
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
// @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;
|
|
|
|
// }
|
2024-07-08 03:25:18 -07:00
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
fitView();
|
2024-07-08 03:25:18 -07:00
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
function trackOpenWorkflowFromOnboardingTemplate() {
|
2024-07-18 01:59:11 -07:00
|
|
|
telemetry.track(
|
|
|
|
`User opened workflow from onboarding template with ID ${workflow.value.meta?.onboardingId}`,
|
|
|
|
{
|
|
|
|
workflow_id: workflowId.value,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
withPostHog: true,
|
|
|
|
},
|
|
|
|
);
|
2024-07-08 03:25:18 -07:00
|
|
|
}
|
|
|
|
|
2024-07-11 08:29:06 -07:00
|
|
|
/**
|
|
|
|
* Templates
|
|
|
|
*/
|
|
|
|
|
|
|
|
async function openWorkflowTemplate(templateId: string) {
|
|
|
|
resetWorkspace();
|
|
|
|
|
|
|
|
canvasStore.startLoading();
|
|
|
|
canvasStore.setLoadingText(i18n.baseText('nodeView.loadingTemplate'));
|
|
|
|
|
|
|
|
workflowsStore.currentWorkflowExecutions = [];
|
|
|
|
executionsStore.activeExecution = null;
|
|
|
|
|
|
|
|
let data: IWorkflowTemplate | undefined;
|
|
|
|
try {
|
|
|
|
void externalHooks.run('template.requested', { templateId });
|
|
|
|
|
|
|
|
data = await templatesStore.getFixedWorkflowTemplate(templateId);
|
|
|
|
if (!data) {
|
|
|
|
throw new Error(
|
|
|
|
i18n.baseText('nodeView.workflowTemplateWithIdCouldNotBeFound', {
|
|
|
|
interpolate: { templateId },
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
toast.showError(error, i18n.baseText('nodeView.couldntImportWorkflow'));
|
|
|
|
await router.replace({ name: VIEWS.NEW_WORKFLOW });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
trackOpenWorkflowTemplate(templateId);
|
|
|
|
|
|
|
|
isBlankRedirect.value = true;
|
|
|
|
await router.replace({ name: VIEWS.NEW_WORKFLOW, query: { templateId } });
|
|
|
|
|
|
|
|
const convertedNodes = data.workflow.nodes.map(workflowsStore.convertTemplateNodeToNodeUi);
|
|
|
|
|
|
|
|
workflowsStore.setConnections(data.workflow.connections);
|
|
|
|
await addNodes(convertedNodes);
|
|
|
|
await workflowsStore.getNewWorkflowData(data.name, projectsStore.currentProjectId);
|
|
|
|
workflowsStore.addToWorkflowMetadata({ templateId });
|
|
|
|
|
|
|
|
uiStore.stateIsDirty = true;
|
|
|
|
|
|
|
|
canvasStore.stopLoading();
|
|
|
|
|
|
|
|
void externalHooks.run('template.open', {
|
|
|
|
templateId,
|
|
|
|
templateName: data.name,
|
|
|
|
workflow: data.workflow,
|
|
|
|
});
|
2024-07-18 01:59:11 -07:00
|
|
|
|
|
|
|
fitView();
|
2024-07-11 08:29:06 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function trackOpenWorkflowTemplate(templateId: string) {
|
|
|
|
telemetry.track(
|
|
|
|
'User inserted workflow template',
|
|
|
|
{
|
|
|
|
source: 'workflow',
|
|
|
|
template_id: tryToParseNumber(templateId),
|
|
|
|
wf_template_repo_session_id: templatesStore.previousSessionId,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
withPostHog: true,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
/**
|
|
|
|
* Nodes
|
|
|
|
*/
|
|
|
|
|
2024-07-11 03:03:46 -07:00
|
|
|
const triggerNodes = computed(() => {
|
|
|
|
return editableWorkflow.value.nodes.filter(
|
|
|
|
(node) => node.type === START_NODE_TYPE || nodeTypesStore.isTriggerNode(node.type),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
const containsTriggerNodes = computed(() => triggerNodes.value.length > 0);
|
|
|
|
|
|
|
|
const allTriggerNodesDisabled = computed(() => {
|
|
|
|
const disabledTriggerNodes = triggerNodes.value.filter((node) => node.disabled);
|
|
|
|
return disabledTriggerNodes.length === triggerNodes.value.length;
|
|
|
|
});
|
|
|
|
|
2024-07-23 00:16:56 -07:00
|
|
|
function onUpdateNodesPosition(events: CanvasNodeMoveEvent[]) {
|
|
|
|
updateNodesPosition(events, { trackHistory: true });
|
|
|
|
}
|
|
|
|
|
2024-07-11 03:03:46 -07:00
|
|
|
function onUpdateNodePosition(id: string, position: CanvasNode['position']) {
|
2024-06-04 05:36:27 -07:00
|
|
|
updateNodePosition(id, position, { trackHistory: true });
|
|
|
|
}
|
|
|
|
|
2024-07-23 00:16:56 -07:00
|
|
|
function onRevertNodePosition({ nodeName, position }: { nodeName: string; position: XYPosition }) {
|
|
|
|
revertUpdateNodePosition(nodeName, { x: position[0], y: position[1] });
|
|
|
|
}
|
|
|
|
|
2024-06-04 05:36:27 -07:00
|
|
|
function onDeleteNode(id: string) {
|
|
|
|
deleteNode(id, { trackHistory: true });
|
|
|
|
}
|
|
|
|
|
2024-07-18 04:00:54 -07:00
|
|
|
function onDeleteNodes(ids: string[]) {
|
|
|
|
deleteNodes(ids);
|
|
|
|
}
|
|
|
|
|
2024-06-04 05:36:27 -07:00
|
|
|
function onRevertDeleteNode({ node }: { node: INodeUi }) {
|
|
|
|
revertDeleteNode(node);
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
|
|
|
|
2024-06-26 06:56:58 -07:00
|
|
|
function onToggleNodeDisabled(id: string) {
|
|
|
|
if (!checkIfEditingIsAllowed()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-07-18 04:00:54 -07:00
|
|
|
toggleNodesDisabled([id]);
|
|
|
|
}
|
|
|
|
|
2024-07-22 23:49:37 -07:00
|
|
|
function onRevertToggleNodeDisabled({ nodeName }: { nodeName: string }) {
|
|
|
|
revertToggleNodeDisabled(nodeName);
|
|
|
|
}
|
|
|
|
|
2024-07-18 04:00:54 -07:00
|
|
|
function onToggleNodesDisabled(ids: string[]) {
|
|
|
|
if (!checkIfEditingIsAllowed()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleNodesDisabled(ids);
|
2024-06-26 06:56:58 -07:00
|
|
|
}
|
|
|
|
|
2024-06-17 05:46:55 -07:00
|
|
|
function onSetNodeActive(id: string) {
|
|
|
|
setNodeActive(id);
|
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
function onSetNodeSelected(id?: string) {
|
|
|
|
setNodeSelected(id);
|
|
|
|
}
|
|
|
|
|
2024-07-19 05:49:52 -07:00
|
|
|
async function onCopyNodes(ids: string[]) {
|
|
|
|
await copyNodes(ids);
|
|
|
|
|
|
|
|
toast.showMessage({ title: i18n.baseText('generic.copiedToClipboard'), type: 'success' });
|
|
|
|
}
|
|
|
|
|
|
|
|
async function onClipboardPaste(plainTextData: string): Promise<void> {
|
|
|
|
if (getNodeViewTab(route) !== MAIN_HEADER_TABS.WORKFLOW) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!checkIfEditingIsAllowed()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let workflowData: IWorkflowDataUpdate | null | undefined = null;
|
|
|
|
|
|
|
|
// Check if it is an URL which could contain workflow data
|
|
|
|
if (plainTextData.match(VALID_WORKFLOW_IMPORT_URL_REGEX)) {
|
|
|
|
const importConfirm = await message.confirm(
|
|
|
|
i18n.baseText('nodeView.confirmMessage.onClipboardPasteEvent.message', {
|
|
|
|
interpolate: { plainTextData },
|
|
|
|
}),
|
|
|
|
i18n.baseText('nodeView.confirmMessage.onClipboardPasteEvent.headline'),
|
|
|
|
{
|
|
|
|
type: 'warning',
|
|
|
|
confirmButtonText: i18n.baseText(
|
|
|
|
'nodeView.confirmMessage.onClipboardPasteEvent.confirmButtonText',
|
|
|
|
),
|
|
|
|
cancelButtonText: i18n.baseText(
|
|
|
|
'nodeView.confirmMessage.onClipboardPasteEvent.cancelButtonText',
|
|
|
|
),
|
|
|
|
dangerouslyUseHTMLString: true,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
if (importConfirm !== MODAL_CONFIRM) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
workflowData = await fetchWorkflowDataFromUrl(plainTextData);
|
|
|
|
} else {
|
|
|
|
// Pasted data is is possible workflow data
|
|
|
|
workflowData = jsonParse<IWorkflowDataUpdate | null>(plainTextData, { fallbackValue: null });
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!workflowData) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = await importWorkflowData(workflowData, 'paste', false);
|
|
|
|
selectNodes(result.nodes?.map((node) => node.id) ?? []);
|
2024-07-18 04:00:54 -07:00
|
|
|
}
|
|
|
|
|
2024-07-19 05:49:52 -07:00
|
|
|
async function onCutNodes(ids: string[]) {
|
|
|
|
if (isCanvasReadOnly.value) {
|
|
|
|
await copyNodes(ids);
|
|
|
|
} else {
|
|
|
|
await cutNodes(ids);
|
|
|
|
}
|
2024-07-18 04:00:54 -07:00
|
|
|
}
|
|
|
|
|
2024-07-19 05:49:52 -07:00
|
|
|
async function onDuplicateNodes(ids: string[]) {
|
|
|
|
if (!checkIfEditingIsAllowed()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const newIds = await duplicateNodes(ids);
|
|
|
|
|
|
|
|
selectNodes(newIds);
|
2024-07-18 04:00:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function onPinNodes(ids: string[], source: PinDataSource) {
|
|
|
|
if (!checkIfEditingIsAllowed()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleNodesPinned(ids, source);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function onSaveWorkflow() {
|
2024-07-19 04:40:00 -07:00
|
|
|
const saved = await workflowHelpers.saveCurrentWorkflow();
|
|
|
|
if (saved) {
|
|
|
|
canvasEventBus.emit('saved:workflow');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function addWorkflowSavedEventBindings() {
|
|
|
|
canvasEventBus.on('saved:workflow', npsSurveyStore.fetchPromptsData);
|
|
|
|
canvasEventBus.on('saved:workflow', onSaveFromWithinNDV);
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeWorkflowSavedEventBindings() {
|
|
|
|
canvasEventBus.off('saved:workflow', npsSurveyStore.fetchPromptsData);
|
|
|
|
canvasEventBus.off('saved:workflow', onSaveFromWithinNDV);
|
|
|
|
canvasEventBus.off('saved:workflow', onSaveFromWithinExecutionDebug);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function onSaveFromWithinNDV() {
|
|
|
|
if (ndvStore.activeNodeName) {
|
|
|
|
toast.showMessage({
|
|
|
|
title: i18n.baseText('generic.workflowSaved'),
|
|
|
|
type: 'success',
|
|
|
|
});
|
|
|
|
}
|
2024-07-18 04:00:54 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async function onCreateWorkflow() {
|
|
|
|
await router.push({ name: VIEWS.NEW_WORKFLOW });
|
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
function onRenameNode(parameterData: IUpdateInformation) {
|
|
|
|
if (parameterData.name === 'name' && parameterData.oldValue) {
|
|
|
|
void renameNode(parameterData.oldValue as string, parameterData.value as string);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-18 04:00:54 -07:00
|
|
|
async function onOpenRenameNodeModal(id: string) {
|
|
|
|
const currentName = workflowsStore.getNodeById(id)?.name ?? '';
|
|
|
|
try {
|
|
|
|
const promptResponsePromise = message.prompt(
|
|
|
|
i18n.baseText('nodeView.prompt.newName') + ':',
|
|
|
|
i18n.baseText('nodeView.prompt.renameNode') + `: ${currentName}`,
|
|
|
|
{
|
|
|
|
customClass: 'rename-prompt',
|
|
|
|
confirmButtonText: i18n.baseText('nodeView.prompt.rename'),
|
|
|
|
cancelButtonText: i18n.baseText('nodeView.prompt.cancel'),
|
|
|
|
inputErrorMessage: i18n.baseText('nodeView.prompt.invalidName'),
|
|
|
|
inputValue: currentName,
|
|
|
|
inputValidator: (value: string) => {
|
|
|
|
if (!value.trim()) {
|
|
|
|
return i18n.baseText('nodeView.prompt.invalidName');
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// Wait till input is displayed
|
|
|
|
await nextTick();
|
|
|
|
|
|
|
|
// Focus and select input content
|
|
|
|
const nameInput = document.querySelector<HTMLInputElement>('.rename-prompt .el-input__inner');
|
|
|
|
nameInput?.focus();
|
|
|
|
nameInput?.select();
|
|
|
|
|
|
|
|
const promptResponse = await promptResponsePromise;
|
|
|
|
|
|
|
|
if (promptResponse.action === MODAL_CONFIRM) {
|
|
|
|
await renameNode(currentName, promptResponse.value, { trackHistory: true });
|
|
|
|
}
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
async function onRevertRenameNode({
|
|
|
|
currentName,
|
|
|
|
newName,
|
|
|
|
}: {
|
|
|
|
currentName: string;
|
|
|
|
newName: string;
|
|
|
|
}) {
|
|
|
|
await revertRenameNode(currentName, newName);
|
|
|
|
}
|
|
|
|
|
2024-07-15 03:00:52 -07:00
|
|
|
function onUpdateNodeParameters(id: string, parameters: Record<string, unknown>) {
|
|
|
|
setNodeParameters(id, parameters);
|
|
|
|
}
|
|
|
|
|
2024-07-18 09:01:14 -07:00
|
|
|
function onClickNodeAdd(source: string, sourceHandle: string) {
|
|
|
|
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
|
|
|
connection: {
|
|
|
|
source,
|
|
|
|
sourceHandle,
|
|
|
|
},
|
|
|
|
eventSource: NODE_CREATOR_OPEN_SOURCES.PLUS_ENDPOINT,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
/**
|
|
|
|
* Credentials
|
|
|
|
*/
|
|
|
|
|
|
|
|
async function loadCredentials() {
|
|
|
|
let options: { workflowId: string } | { projectId: string };
|
|
|
|
|
|
|
|
if (workflow.value) {
|
|
|
|
options = { workflowId: workflow.value.id };
|
|
|
|
} else {
|
|
|
|
const queryParam =
|
|
|
|
typeof route.query?.projectId === 'string' ? route.query?.projectId : undefined;
|
|
|
|
const projectId = queryParam ?? projectsStore.personalProject?.id;
|
|
|
|
if (projectId === undefined) {
|
|
|
|
throw new Error(
|
|
|
|
'Could not find projectId in the query nor could I find the personal project in the project store',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
options = { projectId };
|
|
|
|
}
|
|
|
|
|
|
|
|
await credentialsStore.fetchAllCredentialsForWorkflow(options);
|
|
|
|
}
|
|
|
|
|
2024-05-23 01:42:10 -07:00
|
|
|
/**
|
2024-06-25 02:11:44 -07:00
|
|
|
* Connections
|
2024-05-23 01:42:10 -07:00
|
|
|
*/
|
2024-06-17 05:46:55 -07:00
|
|
|
|
2024-06-04 05:36:27 -07:00
|
|
|
function onCreateConnection(connection: Connection) {
|
2024-07-22 10:26:47 -07:00
|
|
|
createConnection(connection, { trackHistory: true });
|
|
|
|
}
|
|
|
|
|
|
|
|
function onRevertCreateConnection({ connection }: { connection: [IConnection, IConnection] }) {
|
|
|
|
revertCreateConnection(connection);
|
2024-06-04 05:36:27 -07:00
|
|
|
}
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-10 02:22:31 -07:00
|
|
|
function onCreateConnectionCancelled(event: ConnectStartEvent) {
|
|
|
|
setTimeout(() => {
|
|
|
|
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
2024-07-11 07:05:51 -07:00
|
|
|
connection: {
|
|
|
|
source: event.nodeId,
|
|
|
|
sourceHandle: event.handleId,
|
|
|
|
},
|
2024-07-10 02:22:31 -07:00
|
|
|
eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP,
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-06-04 05:36:27 -07:00
|
|
|
function onDeleteConnection(connection: Connection) {
|
|
|
|
deleteConnection(connection, { trackHistory: true });
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
|
|
|
|
2024-06-04 05:36:27 -07:00
|
|
|
function onRevertDeleteConnection({ connection }: { connection: [IConnection, IConnection] }) {
|
|
|
|
revertDeleteConnection(connection);
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
/**
|
|
|
|
* Import / Export
|
|
|
|
*/
|
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
async function importWorkflowExact({ workflow: workflowData }: { workflow: IWorkflowDataUpdate }) {
|
|
|
|
if (!workflowData.nodes || !workflowData.connections) {
|
|
|
|
throw new Error('Invalid workflow object');
|
|
|
|
}
|
|
|
|
|
|
|
|
resetWorkspace();
|
|
|
|
|
2024-08-13 13:14:06 -07:00
|
|
|
await initializeData();
|
2024-07-18 01:59:11 -07:00
|
|
|
await initializeWorkspace({
|
|
|
|
...workflowData,
|
|
|
|
nodes: NodeViewUtils.getFixedNodesList<INodeUi>(workflowData.nodes),
|
|
|
|
} as IWorkflowDb);
|
|
|
|
|
|
|
|
fitView();
|
|
|
|
}
|
|
|
|
|
|
|
|
async function onImportWorkflowDataEvent(data: IDataObject) {
|
2024-07-19 05:49:52 -07:00
|
|
|
const workflowData = data.data as IWorkflowDataUpdate;
|
|
|
|
await importWorkflowData(workflowData, 'file');
|
2024-07-18 01:59:11 -07:00
|
|
|
|
|
|
|
fitView();
|
2024-07-19 05:49:52 -07:00
|
|
|
selectNodes(workflowData.nodes?.map((node) => node.id) ?? []);
|
2024-07-18 01:59:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async function onImportWorkflowUrlEvent(data: IDataObject) {
|
|
|
|
const workflowData = await fetchWorkflowDataFromUrl(data.url as string);
|
|
|
|
if (!workflowData) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await importWorkflowData(workflowData, 'url');
|
|
|
|
|
|
|
|
fitView();
|
2024-07-19 05:49:52 -07:00
|
|
|
selectNodes(workflowData.nodes?.map((node) => node.id) ?? []);
|
2024-07-18 01:59:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function addImportEventBindings() {
|
|
|
|
nodeViewEventBus.on('importWorkflowData', onImportWorkflowDataEvent);
|
|
|
|
nodeViewEventBus.on('importWorkflowUrl', onImportWorkflowUrlEvent);
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeImportEventBindings() {
|
|
|
|
nodeViewEventBus.off('importWorkflowData', onImportWorkflowDataEvent);
|
|
|
|
nodeViewEventBus.off('importWorkflowUrl', onImportWorkflowUrlEvent);
|
2024-06-25 02:11:44 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Node creator
|
|
|
|
*/
|
|
|
|
|
|
|
|
async function onAddNodesAndConnections(
|
2024-05-23 01:42:10 -07:00
|
|
|
{ nodes, connections }: AddedNodesAndConnections,
|
|
|
|
dragAndDrop = false,
|
|
|
|
position?: XYPosition,
|
|
|
|
) {
|
2024-06-25 02:11:44 -07:00
|
|
|
if (!checkIfEditingIsAllowed()) {
|
|
|
|
return;
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
|
|
|
|
2024-07-22 23:49:53 -07:00
|
|
|
await addNodes(nodes, { dragAndDrop, position, trackHistory: true });
|
2024-08-12 09:17:15 -07:00
|
|
|
await nextTick();
|
2024-07-18 01:59:11 -07:00
|
|
|
|
|
|
|
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,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
2024-06-25 02:11:44 -07:00
|
|
|
});
|
2024-07-11 07:05:51 -07:00
|
|
|
|
2024-07-25 05:26:50 -07:00
|
|
|
addConnections(mappedConnections);
|
2024-07-18 01:59:11 -07:00
|
|
|
|
2024-07-25 05:26:50 -07:00
|
|
|
void nextTick(() => {
|
|
|
|
uiStore.resetLastInteractedWith();
|
|
|
|
});
|
2024-06-25 02:11:44 -07:00
|
|
|
}
|
2024-06-17 05:46:55 -07:00
|
|
|
|
2024-07-22 23:49:53 -07:00
|
|
|
async function onRevertAddNode({ node }: { node: INodeUi }) {
|
|
|
|
await revertAddNode(node.name);
|
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
async function onSwitchActiveNode(nodeName: string) {
|
|
|
|
setNodeActiveByName(nodeName);
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
|
|
|
|
2024-07-10 02:22:31 -07:00
|
|
|
async function onOpenSelectiveNodeCreator(node: string, connectionType: NodeConnectionType) {
|
2024-06-25 02:11:44 -07:00
|
|
|
nodeCreatorStore.openSelectiveNodeCreator({ node, connectionType });
|
|
|
|
}
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-18 04:00:54 -07:00
|
|
|
function onOpenNodeCreatorFromCanvas(source: NodeCreatorOpenSource) {
|
|
|
|
onOpenNodeCreator({ createNodeActive: true, source });
|
|
|
|
}
|
|
|
|
|
2024-07-10 02:22:31 -07:00
|
|
|
function onOpenNodeCreator(options: ToggleNodeCreatorOptions) {
|
2024-06-25 02:11:44 -07:00
|
|
|
nodeCreatorStore.openNodeCreator(options);
|
|
|
|
}
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-18 04:00:54 -07:00
|
|
|
function onCreateSticky() {
|
|
|
|
void onAddNodesAndConnections({ nodes: [{ type: STICKY_NODE_TYPE }], connections: [] });
|
|
|
|
}
|
|
|
|
|
2024-07-11 07:05:51 -07:00
|
|
|
function onClickConnectionAdd(connection: Connection) {
|
|
|
|
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
|
|
|
connection,
|
|
|
|
eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_ACTION,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
/**
|
|
|
|
* Executions
|
|
|
|
*/
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-10 01:53:27 -07:00
|
|
|
const isStoppingExecution = ref(false);
|
|
|
|
|
|
|
|
const isWorkflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
|
|
|
|
const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);
|
|
|
|
|
2024-07-11 03:03:46 -07:00
|
|
|
const isExecutionDisabled = computed(() => {
|
|
|
|
if (
|
|
|
|
containsChatTriggerNodes.value &&
|
|
|
|
isOnlyChatTriggerNodeActive.value &&
|
|
|
|
!chatTriggerNodePinnedData.value
|
|
|
|
) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return !containsTriggerNodes.value || allTriggerNodesDisabled.value;
|
|
|
|
});
|
|
|
|
|
2024-07-10 01:53:27 -07:00
|
|
|
const isStopExecutionButtonVisible = computed(
|
|
|
|
() => isWorkflowRunning.value && !isExecutionWaitingForWebhook.value,
|
|
|
|
);
|
|
|
|
const isStopWaitingForWebhookButtonVisible = computed(
|
|
|
|
() => isWorkflowRunning.value && isExecutionWaitingForWebhook.value,
|
|
|
|
);
|
2024-07-11 06:37:01 -07:00
|
|
|
const isClearExecutionButtonVisible = computed(
|
|
|
|
() =>
|
|
|
|
!isReadOnlyRoute.value &&
|
|
|
|
!isReadOnlyEnvironment.value &&
|
|
|
|
!isWorkflowRunning.value &&
|
|
|
|
!allTriggerNodesDisabled.value &&
|
|
|
|
workflowExecutionData.value,
|
|
|
|
);
|
|
|
|
|
|
|
|
const workflowExecutionData = computed(() => workflowsStore.workflowExecutionData);
|
2024-07-10 01:53:27 -07:00
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
async function onRunWorkflow() {
|
2024-07-08 03:25:18 -07:00
|
|
|
trackRunWorkflow();
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-08 03:25:18 -07:00
|
|
|
await runWorkflow({});
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
|
|
|
|
2024-07-08 03:25:18 -07:00
|
|
|
function trackRunWorkflow() {
|
|
|
|
void workflowHelpers.getWorkflowDataToSave().then((workflowData) => {
|
|
|
|
const telemetryPayload = {
|
|
|
|
workflow_id: workflowId.value,
|
|
|
|
node_graph_string: JSON.stringify(
|
|
|
|
TelemetryHelpers.generateNodesGraph(
|
|
|
|
workflowData as IWorkflowBase,
|
|
|
|
workflowHelpers.getNodeTypes(),
|
|
|
|
{ isCloudDeployment: settingsStore.isCloudDeployment },
|
|
|
|
).nodeGraph,
|
|
|
|
),
|
|
|
|
};
|
|
|
|
telemetry.track('User clicked execute workflow button', telemetryPayload);
|
|
|
|
void externalHooks.run('nodeView.onRunWorkflow', telemetryPayload);
|
|
|
|
});
|
2024-06-25 02:11:44 -07:00
|
|
|
}
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-10 00:56:39 -07:00
|
|
|
async function onRunWorkflowToNode(id: string) {
|
|
|
|
const node = workflowsStore.getNodeById(id);
|
|
|
|
if (!node) return;
|
|
|
|
|
|
|
|
trackRunWorkflowToNode(node);
|
|
|
|
await runWorkflow({ destinationNode: node.name, source: 'Node.executeNode' });
|
|
|
|
}
|
|
|
|
|
|
|
|
function trackRunWorkflowToNode(node: INodeUi) {
|
|
|
|
const telemetryPayload = {
|
|
|
|
node_type: node.type,
|
|
|
|
workflow_id: workflowsStore.workflowId,
|
|
|
|
source: 'canvas',
|
|
|
|
push_ref: ndvStore.pushRef,
|
|
|
|
};
|
|
|
|
|
|
|
|
telemetry.track('User clicked execute node button', telemetryPayload);
|
|
|
|
void externalHooks.run('nodeView.onRunNode', telemetryPayload);
|
|
|
|
}
|
|
|
|
|
2024-07-23 01:27:09 -07:00
|
|
|
async function openExecution(executionId: string) {
|
|
|
|
canvasStore.startLoading();
|
|
|
|
resetWorkspace();
|
|
|
|
|
|
|
|
let data: IExecutionResponse | undefined;
|
|
|
|
try {
|
|
|
|
data = await workflowsStore.getExecution(executionId);
|
|
|
|
} catch (error) {
|
|
|
|
toast.showError(error, i18n.baseText('nodeView.showError.openExecution.title'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (data === undefined) {
|
|
|
|
throw new Error(`Execution with id "${executionId}" could not be found!`);
|
|
|
|
}
|
|
|
|
|
|
|
|
await initializeData();
|
|
|
|
await initializeWorkspace(data.workflowData);
|
|
|
|
workflowsStore.setWorkflowExecutionData(data);
|
|
|
|
|
|
|
|
uiStore.stateIsDirty = false;
|
|
|
|
canvasStore.stopLoading();
|
|
|
|
|
|
|
|
fitView();
|
|
|
|
|
|
|
|
canvasEventBus.emit('open:execution', data);
|
|
|
|
|
|
|
|
void externalHooks.run('execution.open', {
|
|
|
|
workflowId: data.workflowData.id,
|
|
|
|
workflowName: data.workflowData.name,
|
|
|
|
executionId,
|
|
|
|
});
|
|
|
|
|
|
|
|
telemetry.track('User opened read-only execution', {
|
|
|
|
workflow_id: data.workflowData.id,
|
|
|
|
execution_mode: data.mode,
|
|
|
|
execution_finished: data.finished,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function onExecutionOpenedWithError(data: IExecutionResponse) {
|
|
|
|
if (!data.finished && data.data?.resultData?.error) {
|
|
|
|
// Check if any node contains an error
|
|
|
|
let nodeErrorFound = false;
|
|
|
|
if (data.data.resultData.runData) {
|
|
|
|
const runData = data.data.resultData.runData;
|
|
|
|
errorCheck: for (const nodeName of Object.keys(runData)) {
|
|
|
|
for (const taskData of runData[nodeName]) {
|
|
|
|
if (taskData.error) {
|
|
|
|
nodeErrorFound = true;
|
|
|
|
break errorCheck;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
!nodeErrorFound &&
|
|
|
|
(data.data.resultData.error.stack ?? data.data.resultData.error.message)
|
|
|
|
) {
|
|
|
|
// Display some more information for now in console to make debugging easier
|
|
|
|
console.error(`Execution ${data.id} error:`);
|
|
|
|
console.error(data.data.resultData.error.stack);
|
|
|
|
toast.showMessage({
|
|
|
|
title: i18n.baseText('nodeView.showError.workflowError'),
|
|
|
|
message: data.data.resultData.error.message,
|
|
|
|
type: 'error',
|
|
|
|
duration: 0,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function onExecutionOpenedWithWaitTill(data: IExecutionResponse) {
|
|
|
|
if ((data as ExecutionSummary).waitTill) {
|
|
|
|
toast.showMessage({
|
|
|
|
title: i18n.baseText('nodeView.thisExecutionHasntFinishedYet'),
|
|
|
|
message: `<a data-action="reload">${i18n.baseText('nodeView.refresh')}</a> ${i18n.baseText(
|
|
|
|
'nodeView.toSeeTheLatestStatus',
|
|
|
|
)}.<br/> <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.wait/" target="_blank">${i18n.baseText(
|
|
|
|
'nodeView.moreInfo',
|
|
|
|
)}</a>`,
|
|
|
|
type: 'warning',
|
|
|
|
duration: 0,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function addExecutionOpenedEventBindings() {
|
|
|
|
canvasEventBus.on('open:execution', onExecutionOpenedWithError);
|
|
|
|
canvasEventBus.on('open:execution', onExecutionOpenedWithWaitTill);
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeExecutionOpenedEventBindings() {
|
|
|
|
canvasEventBus.off('open:execution', onExecutionOpenedWithError);
|
|
|
|
canvasEventBus.off('open:execution', onExecutionOpenedWithWaitTill);
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
|
|
|
|
2024-07-10 01:53:27 -07:00
|
|
|
async function onStopExecution() {
|
|
|
|
isStoppingExecution.value = true;
|
|
|
|
await stopCurrentExecution();
|
|
|
|
isStoppingExecution.value = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function onStopWaitingForWebhook() {
|
|
|
|
await stopWaitingForWebhook();
|
|
|
|
}
|
|
|
|
|
2024-07-11 06:37:01 -07:00
|
|
|
async function onClearExecutionData() {
|
|
|
|
workflowsStore.workflowExecutionData = null;
|
|
|
|
nodeHelpers.updateNodesExecutionIssues();
|
|
|
|
}
|
|
|
|
|
2024-07-11 03:03:46 -07:00
|
|
|
function onRunWorkflowButtonMouseEnter() {
|
|
|
|
nodeViewEventBus.emit('runWorkflowButton:mouseenter');
|
|
|
|
}
|
|
|
|
|
|
|
|
function onRunWorkflowButtonMouseLeave() {
|
|
|
|
nodeViewEventBus.emit('runWorkflowButton:mouseleave');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Chat
|
|
|
|
*/
|
|
|
|
|
|
|
|
const chatTriggerNode = computed(() => {
|
|
|
|
return editableWorkflow.value.nodes.find((node) => node.type === CHAT_TRIGGER_NODE_TYPE);
|
|
|
|
});
|
|
|
|
|
|
|
|
const containsChatTriggerNodes = computed(() => {
|
|
|
|
return (
|
|
|
|
!isExecutionWaitingForWebhook.value &&
|
|
|
|
!!editableWorkflow.value.nodes.find(
|
|
|
|
(node) =>
|
|
|
|
[MANUAL_CHAT_TRIGGER_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE].includes(node.type) &&
|
|
|
|
node.disabled !== true,
|
|
|
|
)
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
const isOnlyChatTriggerNodeActive = computed(() => {
|
|
|
|
return triggerNodes.value.every((node) => node.disabled || node.type === CHAT_TRIGGER_NODE_TYPE);
|
|
|
|
});
|
|
|
|
|
|
|
|
const chatTriggerNodePinnedData = computed(() => {
|
|
|
|
if (!chatTriggerNode.value) return null;
|
|
|
|
|
|
|
|
return workflowsStore.pinDataByNodeName(chatTriggerNode.value.name);
|
|
|
|
});
|
|
|
|
|
2024-07-24 02:10:02 -07:00
|
|
|
async function onOpenChat() {
|
|
|
|
uiStore.openModal(WORKFLOW_LM_CHAT_MODAL_KEY);
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
workflow_id: workflowId.value,
|
|
|
|
};
|
|
|
|
|
|
|
|
void externalHooks.run('nodeView.onOpenChat', payload);
|
|
|
|
telemetry.track('User clicked chat open button', payload);
|
|
|
|
}
|
|
|
|
|
2024-05-23 01:42:10 -07:00
|
|
|
/**
|
2024-06-25 02:11:44 -07:00
|
|
|
* History events
|
2024-05-23 01:42:10 -07:00
|
|
|
*/
|
2024-06-25 02:11:44 -07:00
|
|
|
|
|
|
|
function addUndoRedoEventBindings() {
|
2024-07-23 00:16:56 -07:00
|
|
|
historyBus.on('nodeMove', onRevertNodePosition);
|
2024-07-22 23:49:53 -07:00
|
|
|
historyBus.on('revertAddNode', onRevertAddNode);
|
2024-06-25 02:11:44 -07:00
|
|
|
historyBus.on('revertRemoveNode', onRevertDeleteNode);
|
2024-07-22 10:26:47 -07:00
|
|
|
historyBus.on('revertAddConnection', onRevertCreateConnection);
|
2024-06-25 02:11:44 -07:00
|
|
|
historyBus.on('revertRemoveConnection', onRevertDeleteConnection);
|
|
|
|
historyBus.on('revertRenameNode', onRevertRenameNode);
|
2024-07-22 23:49:37 -07:00
|
|
|
historyBus.on('enableNodeToggle', onRevertToggleNodeDisabled);
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
function removeUndoRedoEventBindings() {
|
2024-07-23 00:16:56 -07:00
|
|
|
historyBus.off('nodeMove', onRevertNodePosition);
|
2024-07-22 23:49:53 -07:00
|
|
|
historyBus.off('revertAddNode', onRevertAddNode);
|
2024-06-25 02:11:44 -07:00
|
|
|
historyBus.off('revertRemoveNode', onRevertDeleteNode);
|
2024-07-22 10:26:47 -07:00
|
|
|
historyBus.off('revertAddConnection', onRevertCreateConnection);
|
2024-06-25 02:11:44 -07:00
|
|
|
historyBus.off('revertRemoveConnection', onRevertDeleteConnection);
|
|
|
|
historyBus.off('revertRenameNode', onRevertRenameNode);
|
2024-07-22 23:49:37 -07:00
|
|
|
historyBus.off('enableNodeToggle', onRevertToggleNodeDisabled);
|
2024-06-25 02:11:44 -07:00
|
|
|
}
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-07-08 03:25:18 -07:00
|
|
|
/**
|
|
|
|
* Source control
|
|
|
|
*/
|
|
|
|
|
|
|
|
async function onSourceControlPull() {
|
|
|
|
try {
|
|
|
|
await Promise.all([
|
|
|
|
environmentsStore.fetchAllVariables(),
|
|
|
|
tagsStore.fetchAll(),
|
|
|
|
loadCredentials(),
|
|
|
|
]);
|
|
|
|
|
|
|
|
if (workflowId.value !== null && !uiStore.stateIsDirty) {
|
|
|
|
const workflowData = await workflowsStore.fetchWorkflow(workflowId.value);
|
|
|
|
if (workflowData) {
|
2024-07-18 01:59:11 -07:00
|
|
|
titleSet(workflowData.name, 'IDLE');
|
2024-07-08 03:25:18 -07:00
|
|
|
await openWorkflow(workflowData);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function addSourceControlEventBindings() {
|
|
|
|
sourceControlEventBus.on('pull', onSourceControlPull);
|
|
|
|
}
|
|
|
|
|
|
|
|
function removeSourceControlEventBindings() {
|
|
|
|
sourceControlEventBus.off('pull', onSourceControlPull);
|
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
/**
|
|
|
|
* Post message events
|
|
|
|
*/
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
function addPostMessageEventBindings() {
|
|
|
|
window.addEventListener('message', onPostMessageReceived);
|
|
|
|
|
|
|
|
if (window.parent) {
|
|
|
|
window.parent.postMessage(
|
|
|
|
JSON.stringify({ command: 'n8nReady', version: rootStore.versionCli }),
|
|
|
|
'*',
|
|
|
|
);
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
2024-06-25 02:11:44 -07:00
|
|
|
}
|
2024-05-23 01:42:10 -07:00
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
function removePostMessageEventBindings() {
|
|
|
|
window.removeEventListener('message', onPostMessageReceived);
|
2024-05-23 01:42:10 -07:00
|
|
|
}
|
2024-06-17 05:46:55 -07:00
|
|
|
|
2024-07-19 05:49:52 -07:00
|
|
|
async function onPostMessageReceived(messageEvent: MessageEvent) {
|
|
|
|
if (
|
|
|
|
!messageEvent ||
|
|
|
|
typeof messageEvent.data !== 'string' ||
|
|
|
|
!messageEvent.data?.includes?.('"command"')
|
|
|
|
) {
|
2024-06-17 05:46:55 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
2024-07-19 05:49:52 -07:00
|
|
|
const json = JSON.parse(messageEvent.data);
|
2024-06-17 05:46:55 -07:00
|
|
|
if (json && json.command === 'openWorkflow') {
|
|
|
|
try {
|
2024-07-18 01:59:11 -07:00
|
|
|
await importWorkflowExact(json);
|
2024-06-17 05:46:55 -07:00
|
|
|
canOpenNDV.value = json.canOpenNDV ?? true;
|
|
|
|
hideNodeIssues.value = json.hideNodeIssues ?? false;
|
|
|
|
isExecutionPreview.value = false;
|
|
|
|
} catch (e) {
|
|
|
|
if (window.top) {
|
|
|
|
window.top.postMessage(
|
|
|
|
JSON.stringify({
|
|
|
|
command: 'error',
|
|
|
|
message: i18n.baseText('openWorkflow.workflowImportError'),
|
|
|
|
}),
|
|
|
|
'*',
|
|
|
|
);
|
|
|
|
}
|
2024-06-25 02:11:44 -07:00
|
|
|
toast.showError(e, i18n.baseText('openWorkflow.workflowImportError'));
|
2024-06-17 05:46:55 -07:00
|
|
|
}
|
|
|
|
} else if (json && json.command === 'openExecution') {
|
|
|
|
try {
|
|
|
|
// If this NodeView is used in preview mode (in iframe) it will not have access to the main app store
|
|
|
|
// so everything it needs has to be sent using post messages and passed down to child components
|
|
|
|
isProductionExecutionPreview.value = json.executionMode !== 'manual';
|
|
|
|
|
|
|
|
await openExecution(json.executionId);
|
|
|
|
canOpenNDV.value = json.canOpenNDV ?? true;
|
|
|
|
hideNodeIssues.value = json.hideNodeIssues ?? false;
|
|
|
|
isExecutionPreview.value = true;
|
|
|
|
} catch (e) {
|
|
|
|
if (window.top) {
|
|
|
|
window.top.postMessage(
|
|
|
|
JSON.stringify({
|
|
|
|
command: 'error',
|
|
|
|
message: i18n.baseText('nodeView.showError.openExecution.title'),
|
|
|
|
}),
|
|
|
|
'*',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
toast.showMessage({
|
|
|
|
title: i18n.baseText('nodeView.showError.openExecution.title'),
|
|
|
|
message: (e as Error).message,
|
|
|
|
type: 'error',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if (json?.command === 'setActiveExecution') {
|
|
|
|
executionsStore.activeExecution = (await executionsStore.fetchExecution(
|
|
|
|
json.executionId,
|
|
|
|
)) as ExecutionSummary;
|
|
|
|
}
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
/**
|
|
|
|
* Permission checks
|
|
|
|
*/
|
2024-06-17 05:46:55 -07:00
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
function checkIfEditingIsAllowed(): boolean {
|
|
|
|
if (readOnlyNotification.value?.visible) {
|
|
|
|
return false;
|
|
|
|
}
|
2024-06-17 05:46:55 -07:00
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
if (isReadOnlyRoute.value || isReadOnlyEnvironment.value) {
|
|
|
|
const messageContext = isReadOnlyRoute.value ? 'executions' : 'workflows';
|
|
|
|
readOnlyNotification.value = toast.showMessage({
|
|
|
|
title: i18n.baseText(
|
|
|
|
isReadOnlyEnvironment.value
|
|
|
|
? `readOnlyEnv.showMessage.${messageContext}.title`
|
|
|
|
: 'readOnly.showMessage.executions.title',
|
|
|
|
),
|
|
|
|
message: i18n.baseText(
|
|
|
|
isReadOnlyEnvironment.value
|
|
|
|
? `readOnlyEnv.showMessage.${messageContext}.message`
|
|
|
|
: 'readOnly.showMessage.executions.message',
|
|
|
|
),
|
|
|
|
type: 'info',
|
|
|
|
dangerouslyUseHTMLString: true,
|
|
|
|
}) as unknown as { visible: boolean };
|
2024-06-17 05:46:55 -07:00
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
return false;
|
|
|
|
}
|
2024-06-17 05:46:55 -07:00
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
return true;
|
2024-06-17 05:46:55 -07:00
|
|
|
}
|
|
|
|
|
2024-07-08 03:25:18 -07:00
|
|
|
function checkIfRouteIsAllowed() {
|
|
|
|
if (
|
|
|
|
isReadOnlyEnvironment.value &&
|
|
|
|
[VIEWS.NEW_WORKFLOW, VIEWS.TEMPLATE_IMPORT].find((view) => view === route.name)
|
|
|
|
) {
|
|
|
|
void nextTick(async () => {
|
|
|
|
resetWorkspace();
|
|
|
|
uiStore.stateIsDirty = false;
|
|
|
|
|
|
|
|
await router.replace({ name: VIEWS.HOMEPAGE });
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Debug mode
|
|
|
|
*/
|
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
async function initializeDebugMode() {
|
2024-07-08 03:25:18 -07:00
|
|
|
if (route.name === VIEWS.EXECUTION_DEBUG) {
|
2024-07-18 01:59:11 -07:00
|
|
|
titleSet(workflowsStore.workflowName, 'DEBUG');
|
2024-07-19 04:40:00 -07:00
|
|
|
|
2024-07-08 03:25:18 -07:00
|
|
|
if (!workflowsStore.isInDebugMode) {
|
|
|
|
await applyExecutionData(route.params.executionId as string);
|
|
|
|
workflowsStore.isInDebugMode = true;
|
|
|
|
}
|
2024-07-19 04:40:00 -07:00
|
|
|
|
|
|
|
canvasEventBus.on('saved:workflow', onSaveFromWithinExecutionDebug);
|
2024-07-08 03:25:18 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-19 04:40:00 -07:00
|
|
|
async function onSaveFromWithinExecutionDebug() {
|
|
|
|
if (route.name !== VIEWS.EXECUTION_DEBUG) return;
|
|
|
|
|
|
|
|
await router.replace({
|
|
|
|
name: VIEWS.WORKFLOW,
|
|
|
|
params: { name: workflowId.value },
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
/**
|
|
|
|
* Canvas
|
|
|
|
*/
|
|
|
|
|
2024-07-23 01:28:52 -07:00
|
|
|
const viewportTransform = ref<ViewportTransform>({ x: 0, y: 0, zoom: 1 });
|
|
|
|
|
|
|
|
function onViewportChange(event: ViewportTransform) {
|
|
|
|
viewportTransform.value = event;
|
|
|
|
uiStore.nodeViewOffsetPosition = [event.x, event.y];
|
|
|
|
}
|
|
|
|
|
2024-07-18 01:59:11 -07:00
|
|
|
function fitView() {
|
|
|
|
setTimeout(() => canvasEventBus.emit('fitView'));
|
|
|
|
}
|
|
|
|
|
2024-07-19 05:49:52 -07:00
|
|
|
function selectNodes(ids: string[]) {
|
|
|
|
setTimeout(() => canvasEventBus.emit('selectNodes', ids));
|
|
|
|
}
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
/**
|
|
|
|
* Mouse events
|
|
|
|
*/
|
2024-06-17 05:46:55 -07:00
|
|
|
|
2024-07-11 03:03:46 -07:00
|
|
|
function onClickPane(position: CanvasNode['position']) {
|
2024-06-25 02:11:44 -07:00
|
|
|
lastClickPosition.value = [position.x, position.y];
|
|
|
|
canvasStore.newNodeInsertPosition = [position.x, position.y];
|
2024-07-18 01:45:19 -07:00
|
|
|
uiStore.isCreateNodeActive = false;
|
2024-06-17 05:46:55 -07:00
|
|
|
}
|
2024-06-25 02:11:44 -07:00
|
|
|
|
2024-07-08 03:25:18 -07:00
|
|
|
/**
|
|
|
|
* Custom Actions
|
|
|
|
*/
|
|
|
|
|
|
|
|
function registerCustomActions() {
|
|
|
|
// @TODO Implement these
|
|
|
|
// this.registerCustomAction({
|
|
|
|
// key: 'openNodeDetail',
|
|
|
|
// action: ({ node }: { node: string }) => {
|
|
|
|
// this.nodeSelectedByName(node, true);
|
|
|
|
// },
|
|
|
|
// });
|
|
|
|
//
|
|
|
|
// this.registerCustomAction({
|
|
|
|
// key: 'openSelectiveNodeCreator',
|
|
|
|
// action: this.openSelectiveNodeCreator,
|
|
|
|
// });
|
|
|
|
//
|
|
|
|
// this.registerCustomAction({
|
|
|
|
// key: 'showNodeCreator',
|
|
|
|
// action: () => {
|
|
|
|
// this.ndvStore.activeNodeName = null;
|
|
|
|
//
|
|
|
|
// void this.$nextTick(() => {
|
|
|
|
// this.showTriggerCreator(NODE_CREATOR_OPEN_SOURCES.TAB);
|
|
|
|
// });
|
|
|
|
// },
|
|
|
|
// });
|
|
|
|
}
|
|
|
|
|
2024-08-13 13:14:06 -07:00
|
|
|
/**
|
|
|
|
* Routing
|
|
|
|
*/
|
|
|
|
|
|
|
|
watch(
|
|
|
|
() => route.name,
|
|
|
|
async () => {
|
|
|
|
if (!checkIfEditingIsAllowed()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
await initializeRoute();
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
/**
|
|
|
|
* Lifecycle
|
|
|
|
*/
|
|
|
|
|
2024-07-08 03:25:18 -07:00
|
|
|
onBeforeMount(() => {
|
|
|
|
if (!isDemoRoute.value) {
|
|
|
|
pushConnectionStore.pushConnect();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-06-25 02:11:44 -07:00
|
|
|
onMounted(async () => {
|
2024-07-18 01:59:11 -07:00
|
|
|
canvasStore.startLoading();
|
|
|
|
titleReset();
|
|
|
|
resetWorkspace();
|
|
|
|
|
2024-07-08 03:25:18 -07:00
|
|
|
void initializeData().then(() => {
|
2024-07-18 01:59:11 -07:00
|
|
|
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();
|
2024-07-08 03:25:18 -07:00
|
|
|
|
|
|
|
checkIfRouteIsAllowed();
|
|
|
|
});
|
2024-06-25 02:11:44 -07:00
|
|
|
|
|
|
|
addUndoRedoEventBindings();
|
|
|
|
addPostMessageEventBindings();
|
2024-07-08 03:25:18 -07:00
|
|
|
addSourceControlEventBindings();
|
2024-07-18 01:59:11 -07:00
|
|
|
addImportEventBindings();
|
2024-07-23 01:27:09 -07:00
|
|
|
addExecutionOpenedEventBindings();
|
2024-07-19 04:40:00 -07:00
|
|
|
addWorkflowSavedEventBindings();
|
2024-07-08 03:25:18 -07:00
|
|
|
|
|
|
|
registerCustomActions();
|
2024-07-18 01:59:11 -07:00
|
|
|
|
|
|
|
// @TODO: This currently breaks since front-end hooks are still not updated to work with pinia store
|
|
|
|
void externalHooks.run('nodeView.mount').catch(() => {});
|
2024-06-25 02:11:44 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
removeUndoRedoEventBindings();
|
2024-07-18 01:59:11 -07:00
|
|
|
removePostMessageEventBindings();
|
2024-07-08 03:25:18 -07:00
|
|
|
removeSourceControlEventBindings();
|
2024-07-18 01:59:11 -07:00
|
|
|
removeImportEventBindings();
|
2024-07-23 01:27:09 -07:00
|
|
|
removeExecutionOpenedEventBindings();
|
2024-07-19 04:40:00 -07:00
|
|
|
removeWorkflowSavedEventBindings();
|
2024-06-25 02:11:44 -07:00
|
|
|
});
|
2024-05-23 01:42:10 -07:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<WorkflowCanvas
|
2024-07-18 01:59:11 -07:00
|
|
|
v-if="editableWorkflow && editableWorkflowObject && !isLoading"
|
2024-05-23 01:42:10 -07:00
|
|
|
:workflow="editableWorkflow"
|
|
|
|
:workflow-object="editableWorkflowObject"
|
2024-07-11 03:03:46 -07:00
|
|
|
:fallback-nodes="fallbackNodes"
|
2024-07-11 08:29:06 -07:00
|
|
|
:event-bus="canvasEventBus"
|
2024-07-23 01:27:09 -07:00
|
|
|
:read-only="isCanvasReadOnly"
|
2024-07-23 00:16:56 -07:00
|
|
|
@update:nodes:position="onUpdateNodesPosition"
|
2024-06-04 05:36:27 -07:00
|
|
|
@update:node:position="onUpdateNodePosition"
|
2024-06-17 05:46:55 -07:00
|
|
|
@update:node:active="onSetNodeActive"
|
2024-06-25 02:11:44 -07:00
|
|
|
@update:node:selected="onSetNodeSelected"
|
2024-06-26 06:56:58 -07:00
|
|
|
@update:node:enabled="onToggleNodeDisabled"
|
2024-07-18 04:00:54 -07:00
|
|
|
@update:node:name="onOpenRenameNodeModal"
|
2024-07-15 03:00:52 -07:00
|
|
|
@update:node:parameters="onUpdateNodeParameters"
|
2024-07-18 09:01:14 -07:00
|
|
|
@click:node:add="onClickNodeAdd"
|
2024-07-10 00:56:39 -07:00
|
|
|
@run:node="onRunWorkflowToNode"
|
2024-06-04 05:36:27 -07:00
|
|
|
@delete:node="onDeleteNode"
|
|
|
|
@create:connection="onCreateConnection"
|
2024-07-10 02:22:31 -07:00
|
|
|
@create:connection:cancelled="onCreateConnectionCancelled"
|
2024-06-04 05:36:27 -07:00
|
|
|
@delete:connection="onDeleteConnection"
|
2024-07-11 07:05:51 -07:00
|
|
|
@click:connection:add="onClickConnectionAdd"
|
2024-06-25 02:11:44 -07:00
|
|
|
@click:pane="onClickPane"
|
2024-07-18 04:00:54 -07:00
|
|
|
@create:node="onOpenNodeCreatorFromCanvas"
|
|
|
|
@create:sticky="onCreateSticky"
|
|
|
|
@delete:nodes="onDeleteNodes"
|
|
|
|
@update:nodes:enabled="onToggleNodesDisabled"
|
|
|
|
@update:nodes:pin="onPinNodes"
|
|
|
|
@duplicate:nodes="onDuplicateNodes"
|
|
|
|
@copy:nodes="onCopyNodes"
|
|
|
|
@cut:nodes="onCutNodes"
|
|
|
|
@run:workflow="onRunWorkflow"
|
|
|
|
@save:workflow="onSaveWorkflow"
|
|
|
|
@create:workflow="onCreateWorkflow"
|
2024-07-23 01:28:52 -07:00
|
|
|
@viewport-change="onViewportChange"
|
2024-05-23 01:42:10 -07:00
|
|
|
>
|
|
|
|
<div :class="$style.executionButtons">
|
2024-07-11 03:03:46 -07:00
|
|
|
<CanvasRunWorkflowButton
|
2024-07-10 01:53:27 -07:00
|
|
|
:waiting-for-webhook="isExecutionWaitingForWebhook"
|
2024-07-11 03:03:46 -07:00
|
|
|
:disabled="isExecutionDisabled"
|
2024-07-10 01:53:27 -07:00
|
|
|
:executing="isWorkflowRunning"
|
2024-07-11 03:03:46 -07:00
|
|
|
@mouseenter="onRunWorkflowButtonMouseEnter"
|
|
|
|
@mouseleave="onRunWorkflowButtonMouseLeave"
|
2024-07-10 01:53:27 -07:00
|
|
|
@click="onRunWorkflow"
|
|
|
|
/>
|
2024-07-24 02:10:02 -07:00
|
|
|
<CanvasChatButton v-if="containsChatTriggerNodes" @click="onOpenChat" />
|
2024-07-10 01:53:27 -07:00
|
|
|
<CanvasStopCurrentExecutionButton
|
|
|
|
v-if="isStopExecutionButtonVisible"
|
|
|
|
:stopping="isStoppingExecution"
|
|
|
|
@click="onStopExecution"
|
|
|
|
/>
|
|
|
|
<CanvasStopWaitingForWebhookButton
|
|
|
|
v-if="isStopWaitingForWebhookButtonVisible"
|
|
|
|
@click="onStopWaitingForWebhook"
|
|
|
|
/>
|
2024-07-11 06:37:01 -07:00
|
|
|
<CanvasClearExecutionDataButton
|
|
|
|
v-if="isClearExecutionButtonVisible"
|
|
|
|
@click="onClearExecutionData"
|
|
|
|
/>
|
2024-05-23 01:42:10 -07:00
|
|
|
</div>
|
|
|
|
<Suspense>
|
2024-07-23 03:30:29 -07:00
|
|
|
<LazyNodeCreation
|
2024-07-23 01:27:09 -07:00
|
|
|
v-if="!isCanvasReadOnly"
|
2024-05-23 01:42:10 -07:00
|
|
|
:create-node-active="uiStore.isCreateNodeActive"
|
2024-07-23 01:28:52 -07:00
|
|
|
:node-view-scale="viewportTransform.zoom"
|
2024-07-10 02:22:31 -07:00
|
|
|
@toggle-node-creator="onOpenNodeCreator"
|
2024-06-25 02:11:44 -07:00
|
|
|
@add-nodes="onAddNodesAndConnections"
|
2024-05-23 01:42:10 -07:00
|
|
|
/>
|
|
|
|
</Suspense>
|
2024-06-17 05:46:55 -07:00
|
|
|
<Suspense>
|
2024-07-23 03:30:29 -07:00
|
|
|
<LazyNodeDetailsView
|
2024-06-27 09:50:09 -07:00
|
|
|
:workflow-object="editableWorkflowObject"
|
2024-07-23 01:27:09 -07:00
|
|
|
:read-only="isCanvasReadOnly"
|
2024-06-17 05:46:55 -07:00
|
|
|
:is-production-execution-preview="isProductionExecutionPreview"
|
2024-06-18 10:15:12 -07:00
|
|
|
:renaming="false"
|
2024-06-25 02:11:44 -07:00
|
|
|
@value-changed="onRenameNode"
|
2024-07-10 01:53:27 -07:00
|
|
|
@stop-execution="onStopExecution"
|
2024-06-25 02:11:44 -07:00
|
|
|
@switch-selected-node="onSwitchActiveNode"
|
2024-07-10 02:22:31 -07:00
|
|
|
@open-connection-node-creator="onOpenSelectiveNodeCreator"
|
2024-07-19 04:40:00 -07:00
|
|
|
@save-keyboard-shortcut="onSaveWorkflow"
|
2024-06-17 05:46:55 -07:00
|
|
|
/>
|
|
|
|
<!--
|
|
|
|
:renaming="renamingActive"
|
|
|
|
-->
|
|
|
|
</Suspense>
|
2024-05-23 01:42:10 -07:00
|
|
|
</WorkflowCanvas>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<style lang="scss" module>
|
|
|
|
.executionButtons {
|
|
|
|
position: absolute;
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
align-items: center;
|
|
|
|
left: 50%;
|
|
|
|
transform: translateX(-50%);
|
|
|
|
bottom: var(--spacing-l);
|
|
|
|
width: auto;
|
|
|
|
|
|
|
|
@media (max-width: $breakpoint-2xs) {
|
|
|
|
bottom: 150px;
|
|
|
|
}
|
|
|
|
|
|
|
|
button {
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
align-items: center;
|
|
|
|
margin-left: 0.625rem;
|
|
|
|
|
|
|
|
&:first-child {
|
|
|
|
margin: 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|