mirror of
https://github.com/n8n-io/n8n.git
synced 2026-07-27 19:15:01 +02:00
fix(editor): Inject instance UTM parameters in all links to the templates website (#34790)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ecd09aa09d
commit
d5b5d8ead5
|
|
@ -10,6 +10,11 @@ vi.mock('@/app/composables/useTelemetry', () => ({
|
|||
useTelemetry: () => ({ track: telemetryTrack }),
|
||||
}));
|
||||
|
||||
const websiteTemplateRepositoryURL = 'https://n8n.io/workflows?utm_instance=test';
|
||||
vi.mock('@/features/workflows/templates/templates.store', () => ({
|
||||
useTemplatesStore: () => ({ websiteTemplateRepositoryURL }),
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(WorkflowPreviewSuggestions, {
|
||||
props: {
|
||||
suggestions,
|
||||
|
|
@ -48,4 +53,11 @@ describe('WorkflowPreviewSuggestions', () => {
|
|||
suggestion_id: suggestion.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('links "see all" to the templates website URL with instance parameters', () => {
|
||||
const { container } = renderComponent();
|
||||
|
||||
const link = container.querySelector('a');
|
||||
expect(link).toHaveAttribute('href', websiteTemplateRepositoryURL);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { N8nIcon } from '@n8n/design-system';
|
|||
import { useI18n, type BaseTextKey } from '@n8n/i18n';
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
import { useTelemetry } from '@/app/composables/useTelemetry';
|
||||
import { useTemplatesStore } from '@/features/workflows/templates/templates.store';
|
||||
import { type WorkflowPreviewSuggestion } from '../suggestions';
|
||||
|
||||
const PREVIEW_HOVER_DELAY_MS = 30;
|
||||
|
|
@ -27,6 +28,7 @@ const emit = defineEmits<{
|
|||
|
||||
const i18n = useI18n();
|
||||
const telemetry = useTelemetry();
|
||||
const templatesStore = useTemplatesStore();
|
||||
const activePreview = ref<string | null>(null);
|
||||
let hoverTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const hoverStartTimes = new Map<string, number>();
|
||||
|
|
@ -123,7 +125,7 @@ onUnmounted(clearPreview);
|
|||
<span>{{ i18n.baseText(suggestion.labelKey) }}</span>
|
||||
</button>
|
||||
<a
|
||||
href="https://n8n.io/workflows/"
|
||||
:href="templatesStore.websiteTemplateRepositoryURL"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:class="$style.seeAllLink"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { APP_MODALS_ELEMENT_ID, EXPERIMENT_TEMPLATE_RECO_V2_KEY } from '@/app/constants';
|
||||
import { STORES } from '@n8n/stores';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { vi } from 'vitest';
|
||||
import NodeRecommendationModal from './NodeRecommendationModal.vue';
|
||||
|
||||
vi.mock('../stores/templateRecoV2.store', () => ({
|
||||
usePersonalizedTemplatesV2Store: () => ({
|
||||
nodes: ['n8n-nodes-base.slack'],
|
||||
getNodeData: () => ({ youtube: [], starter: [], popular: [] }),
|
||||
getTemplateData: vi.fn(),
|
||||
trackModalTabView: vi.fn(),
|
||||
trackSeeMoreClick: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/stores/nodeTypes.store', () => ({
|
||||
useNodeTypesStore: () => ({
|
||||
getNodeType: (name: string) => ({ name, displayName: 'Slack' }),
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/workflows/templates/templates.store', () => ({
|
||||
useTemplatesStore: () => ({
|
||||
websiteTemplateRepositoryParameters: new URLSearchParams({
|
||||
utm_source: 'n8n_app',
|
||||
utm_instance: 'https://test.instance/',
|
||||
}),
|
||||
constructTemplateRepositoryURL: (params: URLSearchParams) =>
|
||||
`https://n8n.io/workflows/?${params.toString()}`,
|
||||
}),
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(NodeRecommendationModal, {
|
||||
props: {
|
||||
modalName: EXPERIMENT_TEMPLATE_RECO_V2_KEY,
|
||||
data: {},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
NodeIcon: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('NodeRecommendationModal', () => {
|
||||
beforeEach(() => {
|
||||
const modalsContainer = document.createElement('div');
|
||||
modalsContainer.id = APP_MODALS_ELEMENT_ID;
|
||||
document.body.appendChild(modalsContainer);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.getElementById(APP_MODALS_ELEMENT_ID)?.remove();
|
||||
});
|
||||
|
||||
it('builds "see more" links from the template repository parameters', async () => {
|
||||
const { findByText } = renderComponent({
|
||||
pinia: createTestingPinia({
|
||||
initialState: {
|
||||
[STORES.UI]: {
|
||||
modalsById: {
|
||||
[EXPERIMENT_TEMPLATE_RECO_V2_KEY]: { open: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const starterLink = (await findByText('See more starter templates')).closest('a');
|
||||
expect(starterLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://n8n.io/workflows/?utm_source=n8n_app&utm_instance=https%3A%2F%2Ftest.instance%2F&integrations=Slack&q=Simple',
|
||||
);
|
||||
|
||||
const popularLink = (await findByText('See more popular templates')).closest('a');
|
||||
expect(popularLink).toHaveAttribute(
|
||||
'href',
|
||||
'https://n8n.io/workflows/?utm_source=n8n_app&utm_instance=https%3A%2F%2Ftest.instance%2F&integrations=Slack',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import Modal from '@/app/components/Modal.vue';
|
||||
import { EXPERIMENT_TEMPLATE_RECO_V2_KEY, TEMPLATES_URLS } from '@/app/constants';
|
||||
import { EXPERIMENT_TEMPLATE_RECO_V2_KEY } from '@/app/constants';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import { useUIStore } from '@/app/stores/ui.store';
|
||||
import { useTemplatesStore } from '@/features/workflows/templates/templates.store';
|
||||
import type { ITemplatesWorkflowFull } from '@n8n/rest-api-client';
|
||||
import { computed, ref, watchEffect } from 'vue';
|
||||
import { usePersonalizedTemplatesV2Store } from '../stores/templateRecoV2.store';
|
||||
|
|
@ -28,6 +29,7 @@ const {
|
|||
trackSeeMoreClick,
|
||||
} = usePersonalizedTemplatesV2Store();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const templatesStore = useTemplatesStore();
|
||||
const locale = useI18n();
|
||||
|
||||
const closeModal = () => {
|
||||
|
|
@ -65,14 +67,17 @@ const youtubeVideos = computed(() => {
|
|||
|
||||
const starterLink = computed(() => {
|
||||
const name = nodeTypes.value.get(selectedNode.value)?.displayName ?? '';
|
||||
const encodedName = encodeURIComponent(name);
|
||||
return `${TEMPLATES_URLS.BASE_WEBSITE_URL}?integrations=${encodedName}&q=Simple`;
|
||||
const params = new URLSearchParams(templatesStore.websiteTemplateRepositoryParameters);
|
||||
params.set('integrations', name);
|
||||
params.set('q', 'Simple');
|
||||
return templatesStore.constructTemplateRepositoryURL(params);
|
||||
});
|
||||
|
||||
const popularLink = computed(() => {
|
||||
const name = nodeTypes.value.get(selectedNode.value)?.displayName ?? '';
|
||||
const encodedName = encodeURIComponent(name);
|
||||
return `${TEMPLATES_URLS.BASE_WEBSITE_URL}?integrations=${encodedName}`;
|
||||
const params = new URLSearchParams(templatesStore.websiteTemplateRepositoryParameters);
|
||||
params.set('integrations', name);
|
||||
return templatesStore.constructTemplateRepositoryURL(params);
|
||||
});
|
||||
|
||||
function onSelectedNodeChange(val: string) {
|
||||
|
|
|
|||
|
|
@ -142,6 +142,24 @@ describe('viewsData', () => {
|
|||
|
||||
expect(messageAgentItem).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should not mutate the shared template repository parameters', () => {
|
||||
const templatesStore = useTemplatesStore();
|
||||
const sharedParams = new URLSearchParams({ test: 'value' });
|
||||
vi.spyOn(templatesStore, 'websiteTemplateRepositoryParameters', 'get').mockReturnValue(
|
||||
sharedParams,
|
||||
);
|
||||
|
||||
AIView([]);
|
||||
AIView([]);
|
||||
|
||||
expect(sharedParams.has('utm_user_role')).toBe(false);
|
||||
|
||||
const [lastCallParams] = vi
|
||||
.mocked(templatesStore.constructTemplateRepositoryURL)
|
||||
.mock.calls.at(-1)!;
|
||||
expect(lastCallParams.toString()).toBe('test=value&utm_user_role=AdvancedAI');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HitlToolView', () => {
|
||||
|
|
|
|||
|
|
@ -198,8 +198,10 @@ export function AIView(_nodes: SimplifiedNodeType[]): NodeView {
|
|||
const agentNodes = getAiNodesBySubcategory(nodeTypesStore.allLatestNodeTypes, AI_CATEGORY_AGENTS);
|
||||
const messageAnAgentNode = getMessageAnAgentNode(nodeTypesStore, settingsStore);
|
||||
|
||||
const websiteCategoryURLParams = templatesStore.websiteTemplateRepositoryParameters;
|
||||
websiteCategoryURLParams.append('utm_user_role', 'AdvancedAI');
|
||||
const websiteCategoryURLParams = new URLSearchParams(
|
||||
templatesStore.websiteTemplateRepositoryParameters,
|
||||
);
|
||||
websiteCategoryURLParams.set('utm_user_role', 'AdvancedAI');
|
||||
const aiTemplatesURL = templatesStore.constructTemplateRepositoryURL(
|
||||
websiteCategoryURLParams,
|
||||
TEMPLATE_CATEGORY_AI,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user