mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 13:27:31 -08:00
feat(editor): Open template credential setup from collection (#7882)
Open the template credential setup when using a template from the collection view. NOTE! This feature is still behind a feature flag. To test, set: ```js localStorage.setItem('template-credentials-setup', 'true') ``` https://github.com/n8n-io/n8n/assets/10324676/46ccec7c-5a44-429e-99ad-1c10640e99e5
This commit is contained in:
parent
67702c2485
commit
627ddb91fb
|
@ -1,4 +1,9 @@
|
||||||
import { CredentialsModal, MessageBox } from '../pages/modals';
|
import { CredentialsModal, MessageBox } from '../pages/modals';
|
||||||
|
import {
|
||||||
|
clickUseWorkflowButtonByTitle,
|
||||||
|
visitTemplateCollectionPage,
|
||||||
|
testData,
|
||||||
|
} from '../pages/template-collection';
|
||||||
import { TemplateCredentialSetupPage } from '../pages/template-credential-setup';
|
import { TemplateCredentialSetupPage } from '../pages/template-credential-setup';
|
||||||
import { TemplateWorkflowPage } from '../pages/template-workflow';
|
import { TemplateWorkflowPage } from '../pages/template-workflow';
|
||||||
import { WorkflowPage } from '../pages/workflow';
|
import { WorkflowPage } from '../pages/workflow';
|
||||||
|
@ -28,6 +33,16 @@ describe('Template credentials setup', () => {
|
||||||
.should('be.visible');
|
.should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can be opened from template collection page', () => {
|
||||||
|
visitTemplateCollectionPage(testData.ecommerceStarterPack);
|
||||||
|
templateCredentialsSetupPage.actions.enableFeatureFlag();
|
||||||
|
clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram');
|
||||||
|
|
||||||
|
templateCredentialsSetupPage.getters
|
||||||
|
.title(`Setup 'Promote new Shopify products on Twitter and Telegram' template`)
|
||||||
|
.should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
it('can be opened with a direct url', () => {
|
it('can be opened with a direct url', () => {
|
||||||
templateCredentialsSetupPage.actions.visit(testTemplate.id);
|
templateCredentialsSetupPage.actions.visit(testTemplate.id);
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
33
cypress/pages/template-collection.ts
Normal file
33
cypress/pages/template-collection.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
export function visitTemplateCollectionPage(withFixture: Fixture) {
|
||||||
|
cy.intercept(
|
||||||
|
'GET',
|
||||||
|
`https://api.n8n.io/api/templates/collections/${testData.ecommerceStarterPack.id}`,
|
||||||
|
{
|
||||||
|
fixture: withFixture.fixture,
|
||||||
|
},
|
||||||
|
).as('getTemplateCollection');
|
||||||
|
|
||||||
|
cy.visit(`/collections/${withFixture.id}`);
|
||||||
|
|
||||||
|
cy.wait('@getTemplateCollection');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clickUseWorkflowButtonByTitle(workflowTitle: string) {
|
||||||
|
cy.getByTestId('template-card')
|
||||||
|
.contains('[data-test-id=template-card]', workflowTitle)
|
||||||
|
.realHover({ position: 'center' })
|
||||||
|
.findChildByTestId('use-workflow-button')
|
||||||
|
.click({ force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Fixture = {
|
||||||
|
id: number;
|
||||||
|
fixture: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testData = {
|
||||||
|
ecommerceStarterPack: {
|
||||||
|
id: 1,
|
||||||
|
fixture: 'Ecommerce_starter_pack_template_collection.json',
|
||||||
|
},
|
||||||
|
};
|
|
@ -7,6 +7,7 @@
|
||||||
!loading && $style.loaded,
|
!loading && $style.loaded,
|
||||||
]"
|
]"
|
||||||
@click="onCardClick"
|
@click="onCardClick"
|
||||||
|
data-test-id="template-card"
|
||||||
>
|
>
|
||||||
<div :class="$style.loading" v-if="loading">
|
<div :class="$style.loading" v-if="loading">
|
||||||
<n8n-loading :rows="2" :shrinkLast="false" :loading="loading" />
|
<n8n-loading :rows="2" :shrinkLast="false" :loading="loading" />
|
||||||
|
@ -39,6 +40,7 @@
|
||||||
outline
|
outline
|
||||||
label="Use workflow"
|
label="Use workflow"
|
||||||
@click.stop="onUseWorkflowClick"
|
@click.stop="onUseWorkflowClick"
|
||||||
|
data-test-id="use-workflow-button"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -220,9 +220,7 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
|
||||||
this.currentSessionId = `templates-${Date.now()}`;
|
this.currentSessionId = `templates-${Date.now()}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async fetchTemplateById(
|
async fetchTemplateById(templateId: string): Promise<ITemplatesWorkflowFull> {
|
||||||
templateId: string,
|
|
||||||
): Promise<ITemplatesWorkflow | ITemplatesWorkflowFull> {
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const apiEndpoint: string = settingsStore.templatesHost;
|
const apiEndpoint: string = settingsStore.templatesHost;
|
||||||
const versionCli: string = settingsStore.versionCli;
|
const versionCli: string = settingsStore.versionCli;
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
import type { INodeUi, IWorkflowData, IWorkflowTemplate } from '@/Interface';
|
import type { INodeUi, IWorkflowData, IWorkflowTemplate } from '@/Interface';
|
||||||
import { getNewWorkflow } from '@/api/workflows';
|
import { getNewWorkflow } from '@/api/workflows';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
import type { useRootStore } from '@/stores/n8nRoot.store';
|
import type { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import type { useWorkflowsStore } from '@/stores/workflows.store';
|
import type { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag';
|
||||||
import { getFixedNodesList } from '@/utils/nodeViewUtils';
|
import { getFixedNodesList } from '@/utils/nodeViewUtils';
|
||||||
import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms';
|
import { replaceAllTemplateNodeCredentials } from '@/utils/templates/templateTransforms';
|
||||||
import type { INodeCredentialsDetails } from 'n8n-workflow';
|
import type { INodeCredentialsDetails } from 'n8n-workflow';
|
||||||
|
import type { RouteLocationRaw, Router } from 'vue-router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new workflow from a template
|
* Creates a new workflow from a template
|
||||||
|
@ -35,3 +38,32 @@ export async function createWorkflowFromTemplate(
|
||||||
|
|
||||||
return createdWorkflow;
|
return createdWorkflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the template credential setup view (or workflow view
|
||||||
|
* if the feature flag is disabled)
|
||||||
|
*/
|
||||||
|
export async function openTemplateCredentialSetup(opts: {
|
||||||
|
templateId: string;
|
||||||
|
router: Router;
|
||||||
|
inNewBrowserTab?: boolean;
|
||||||
|
}) {
|
||||||
|
const { router, templateId, inNewBrowserTab = false } = opts;
|
||||||
|
|
||||||
|
const routeLocation: RouteLocationRaw = isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup)
|
||||||
|
? {
|
||||||
|
name: VIEWS.TEMPLATE_SETUP,
|
||||||
|
params: { id: templateId },
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
name: VIEWS.TEMPLATE_IMPORT,
|
||||||
|
params: { id: templateId },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (inNewBrowserTab) {
|
||||||
|
const route = router.resolve(routeLocation);
|
||||||
|
window.open(route.href, '_blank');
|
||||||
|
} else {
|
||||||
|
await router.push(routeLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -69,6 +69,8 @@ import { setPageTitle } from '@/utils/htmlUtils';
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag';
|
||||||
|
import { openTemplateCredentialSetup } from '@/utils/templates/templateActions';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'TemplatesCollectionView',
|
name: 'TemplatesCollectionView',
|
||||||
|
@ -80,11 +82,13 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useTemplatesStore, usePostHog),
|
...mapStores(useTemplatesStore, usePostHog),
|
||||||
collection(): null | ITemplatesCollectionFull {
|
collection(): ITemplatesCollectionFull | null {
|
||||||
return this.templatesStore.getCollectionById(this.collectionId);
|
return this.templatesStore.getCollectionById(this.collectionId);
|
||||||
},
|
},
|
||||||
collectionId(): string {
|
collectionId(): string {
|
||||||
return this.$route.params.id;
|
return Array.isArray(this.$route.params.id)
|
||||||
|
? this.$route.params.id[0]
|
||||||
|
: this.$route.params.id;
|
||||||
},
|
},
|
||||||
collectionWorkflows(): Array<ITemplatesWorkflow | ITemplatesWorkflowFull | null> | null {
|
collectionWorkflows(): Array<ITemplatesWorkflow | ITemplatesWorkflowFull | null> | null {
|
||||||
if (!this.collection) {
|
if (!this.collection) {
|
||||||
|
@ -116,17 +120,24 @@ export default defineComponent({
|
||||||
onOpenTemplate({ event, id }: { event: MouseEvent; id: string }) {
|
onOpenTemplate({ event, id }: { event: MouseEvent; id: string }) {
|
||||||
this.navigateTo(event, VIEWS.TEMPLATE, id);
|
this.navigateTo(event, VIEWS.TEMPLATE, id);
|
||||||
},
|
},
|
||||||
onUseWorkflow({ event, id }: { event: MouseEvent; id: string }) {
|
async onUseWorkflow({ event, id }: { event: MouseEvent; id: string }) {
|
||||||
const telemetryPayload = {
|
if (!isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup)) {
|
||||||
template_id: id,
|
const telemetryPayload = {
|
||||||
wf_template_repo_session_id: this.workflowsStore.currentSessionId,
|
template_id: id,
|
||||||
source: 'collection',
|
wf_template_repo_session_id: this.templatesStore.currentSessionId,
|
||||||
};
|
source: 'collection',
|
||||||
void this.$externalHooks().run('templatesCollectionView.onUseWorkflow', telemetryPayload);
|
};
|
||||||
this.$telemetry.track('User inserted workflow template', telemetryPayload, {
|
await this.$externalHooks().run('templatesCollectionView.onUseWorkflow', telemetryPayload);
|
||||||
withPostHog: true,
|
this.$telemetry.track('User inserted workflow template', telemetryPayload, {
|
||||||
|
withPostHog: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await openTemplateCredentialSetup({
|
||||||
|
router: this.$router,
|
||||||
|
templateId: id,
|
||||||
|
inNewBrowserTab: event.metaKey || event.ctrlKey,
|
||||||
});
|
});
|
||||||
this.navigateTo(event, VIEWS.TEMPLATE_IMPORT, id);
|
|
||||||
},
|
},
|
||||||
navigateTo(e: MouseEvent, page: string, id: string) {
|
navigateTo(e: MouseEvent, page: string, id: string) {
|
||||||
if (e.metaKey || e.ctrlKey) {
|
if (e.metaKey || e.ctrlKey) {
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
data-test-id="use-template-button"
|
data-test-id="use-template-button"
|
||||||
:label="$locale.baseText('template.buttons.useThisWorkflowButton')"
|
:label="$locale.baseText('template.buttons.useThisWorkflowButton')"
|
||||||
size="large"
|
size="large"
|
||||||
@click="openTemplateSetup(template.id, $event)"
|
@click="openTemplateSetup(templateId, $event)"
|
||||||
/>
|
/>
|
||||||
<n8n-loading :loading="!template" :rows="1" variant="button" />
|
<n8n-loading :loading="!template" :rows="1" variant="button" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -63,12 +63,12 @@ import TemplateDetails from '@/components/TemplateDetails.vue';
|
||||||
import TemplatesView from './TemplatesView.vue';
|
import TemplatesView from './TemplatesView.vue';
|
||||||
import WorkflowPreview from '@/components/WorkflowPreview.vue';
|
import WorkflowPreview from '@/components/WorkflowPreview.vue';
|
||||||
|
|
||||||
import type { ITemplatesWorkflow, ITemplatesWorkflowFull } from '@/Interface';
|
import type { ITemplatesWorkflowFull } from '@/Interface';
|
||||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||||
import { setPageTitle } from '@/utils/htmlUtils';
|
import { setPageTitle } from '@/utils/htmlUtils';
|
||||||
import { VIEWS } from '@/constants';
|
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
|
import { openTemplateCredentialSetup } from '@/utils/templates/templateActions';
|
||||||
import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag';
|
import { FeatureFlag, isFeatureFlagEnabled } from '@/utils/featureFlag';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
@ -81,11 +81,13 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapStores(useTemplatesStore, usePostHog),
|
...mapStores(useTemplatesStore, usePostHog),
|
||||||
template(): ITemplatesWorkflow | ITemplatesWorkflowFull {
|
template(): ITemplatesWorkflowFull | null {
|
||||||
return this.templatesStore.getTemplateById(this.templateId);
|
return this.templatesStore.getFullTemplateById(this.templateId);
|
||||||
},
|
},
|
||||||
templateId() {
|
templateId() {
|
||||||
return this.$route.params.id;
|
return Array.isArray(this.$route.params.id)
|
||||||
|
? this.$route.params.id[0]
|
||||||
|
: this.$route.params.id;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -96,35 +98,25 @@ export default defineComponent({
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openTemplateSetup(id: string, e: PointerEvent) {
|
async openTemplateSetup(id: string, e: PointerEvent) {
|
||||||
const telemetryPayload = {
|
if (!isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup)) {
|
||||||
source: 'workflow',
|
const telemetryPayload = {
|
||||||
template_id: id,
|
source: 'workflow',
|
||||||
wf_template_repo_session_id: this.templatesStore.currentSessionId,
|
template_id: id,
|
||||||
};
|
wf_template_repo_session_id: this.templatesStore.currentSessionId,
|
||||||
|
};
|
||||||
|
|
||||||
void this.$externalHooks().run('templatesWorkflowView.openWorkflow', telemetryPayload);
|
this.$telemetry.track('User inserted workflow template', telemetryPayload, {
|
||||||
this.$telemetry.track('User inserted workflow template', telemetryPayload, {
|
withPostHog: true,
|
||||||
withPostHog: true,
|
});
|
||||||
});
|
await this.$externalHooks().run('templatesWorkflowView.openWorkflow', telemetryPayload);
|
||||||
|
|
||||||
if (isFeatureFlagEnabled(FeatureFlag.templateCredentialsSetup)) {
|
|
||||||
if (e.metaKey || e.ctrlKey) {
|
|
||||||
const route = this.$router.resolve({ name: VIEWS.TEMPLATE_SETUP, params: { id } });
|
|
||||||
window.open(route.href, '_blank');
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
void this.$router.push({ name: VIEWS.TEMPLATE_SETUP, params: { id } });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (e.metaKey || e.ctrlKey) {
|
|
||||||
const route = this.$router.resolve({ name: VIEWS.TEMPLATE_IMPORT, params: { id } });
|
|
||||||
window.open(route.href, '_blank');
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
void this.$router.push({ name: VIEWS.TEMPLATE_IMPORT, params: { id } });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await openTemplateCredentialSetup({
|
||||||
|
router: this.$router,
|
||||||
|
templateId: id,
|
||||||
|
inNewBrowserTab: e.metaKey || e.ctrlKey,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onHidePreview() {
|
onHidePreview() {
|
||||||
this.showPreview = false;
|
this.showPreview = false;
|
||||||
|
|
Loading…
Reference in a new issue