import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; import type { Router } from 'vue-router'; import { useCredentialsStore } from '@/stores/credentials.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useRootStore } from '@/stores/n8nRoot.store'; import { useTemplatesStore } from '@/stores/templates.store'; import { useWorkflowsStore } from '@/stores/workflows.store'; import type { INodeTypeDescription } from 'n8n-workflow'; import type { INodeUi } from '@/Interface'; import { VIEWS } from '@/constants'; import { createWorkflowFromTemplate } from '@/utils/templates/templateActions'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { useTelemetry } from '@/composables/useTelemetry'; import { useCredentialSetupState } from '@/views/SetupWorkflowFromTemplateView/useCredentialSetupState'; import { tryToParseNumber } from '@/utils/typesUtils'; export type NodeAndType = { node: INodeUi; nodeType: INodeTypeDescription; }; export type RequiredCredentials = { node: INodeUi; credentialName: string; credentialType: string; }; export type AppCredentialCount = { appName: string; count: number; }; /** * Store for managing the state of the SetupWorkflowFromTemplateView */ export const useSetupTemplateStore = defineStore('setupTemplate', () => { //#region State const templateId = ref(''); const isLoading = ref(true); const isSaving = ref(false); //#endregion State const templatesStore = useTemplatesStore(); const nodeTypesStore = useNodeTypesStore(); const credentialsStore = useCredentialsStore(); const rootStore = useRootStore(); const workflowsStore = useWorkflowsStore(); //#region Getters const template = computed(() => { return templateId.value ? templatesStore.getFullTemplateById(templateId.value) : null; }); const templateNodes = computed(() => { return template.value?.workflow.nodes ?? []; }); const { appCredentials, credentialOverrides, credentialUsages, credentialsByKey, nodesRequiringCredentialsSorted, numFilledCredentials, selectedCredentialIdByKey, setSelectedCredentialId, unsetSelectedCredential, } = useCredentialSetupState(templateNodes); //#endregion Getters //#region Actions const setTemplateId = (id: string) => { templateId.value = id; }; const ignoredAutoFillCredentialTypes = new Set([ 'httpBasicAuth', 'httpCustomAuth', 'httpDigestAuth', 'httpHeaderAuth', 'oAuth1Api', 'oAuth2Api', 'httpQueryAuth', ]); /** * Selects initial credentials for the template. Credentials * need to be loaded before this. */ const setInitialCredentialSelection = () => { for (const credUsage of credentialUsages.value) { if (ignoredAutoFillCredentialTypes.has(credUsage.credentialType)) { continue; } const availableCreds = credentialsStore.getCredentialsByType(credUsage.credentialType); if (availableCreds.length === 1) { selectedCredentialIdByKey.value[credUsage.key] = availableCreds[0].id; } } }; /** * Loads the template if it hasn't been loaded yet. */ const loadTemplateIfNeeded = async () => { if (!!template.value || !templateId.value) { return; } await templatesStore.fetchTemplateById(templateId.value); setInitialCredentialSelection(); }; /** * Initializes the store for a specific template. */ const init = async () => { isLoading.value = true; try { selectedCredentialIdByKey.value = {}; await Promise.all([ credentialsStore.fetchAllCredentials(), credentialsStore.fetchCredentialTypes(false), nodeTypesStore.loadNodeTypesIfNotLoaded(), loadTemplateIfNeeded(), ]); setInitialCredentialSelection(); } finally { isLoading.value = false; } }; /** * Skips the setup and goes directly to the workflow view. */ const skipSetup = async ({ router }: { router: Router }) => { const externalHooks = useExternalHooks(); const telemetry = useTelemetry(); await externalHooks.run('templatesWorkflowView.openWorkflow', { source: 'workflow', template_id: templateId.value, wf_template_repo_session_id: templatesStore.currentSessionId, }); telemetry.track('User closed cred setup', { completed: false, creds_filled: 0, creds_needed: credentialUsages.value.length, workflow_id: null, }); // Replace the URL so back button doesn't come back to this setup view await router.replace({ name: VIEWS.TEMPLATE_IMPORT, params: { id: templateId.value }, }); }; /** * Creates a workflow from the template and navigates to the workflow view. */ const createWorkflow = async (opts: { router: Router }) => { const { router } = opts; const telemetry = useTelemetry(); if (!template.value) { return; } try { isSaving.value = true; const createdWorkflow = await createWorkflowFromTemplate({ template: template.value, credentialOverrides: credentialOverrides.value, rootStore, workflowsStore, nodeTypeProvider: nodeTypesStore, }); telemetry.track('User closed cred setup', { completed: true, creds_filled: numFilledCredentials.value, creds_needed: credentialUsages.value.length, workflow_id: createdWorkflow.id, }); telemetry.track( 'User inserted workflow template', { source: 'workflow', template_id: tryToParseNumber(templateId.value), wf_template_repo_session_id: templatesStore.currentSessionId, }, { withPostHog: true }, ); telemetry.track('User saved new workflow from template', { template_id: tryToParseNumber(templateId.value), workflow_id: createdWorkflow.id, wf_template_repo_session_id: templatesStore.currentSessionId, }); // Replace the URL so back button doesn't come back to this setup view await router.replace({ name: VIEWS.WORKFLOW, params: { name: createdWorkflow.id }, }); } finally { isSaving.value = false; } }; //#endregion Actions return { credentialsByKey, isLoading, isSaving, appCredentials, nodesRequiringCredentialsSorted, template, credentialUsages, selectedCredentialIdByKey, credentialOverrides, numFilledCredentials, createWorkflow, skipSetup, init, loadTemplateIfNeeded, setInitialCredentialSelection, setTemplateId, setSelectedCredentialId, unsetSelectedCredential, }; });