feat(editor): Implement part 2 of ready to run v2 (no-changelog) (#20353)

This commit is contained in:
Romeo Balta 2025-10-03 15:47:13 +01:00 committed by GitHub
parent 0602018d9a
commit 476bfe58b7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 445 additions and 91 deletions

View File

@ -844,6 +844,13 @@ export const READY_TO_RUN_V2_EXPERIMENT = {
variant2: 'variant-2-twoboxes',
};
export const READY_TO_RUN_V2_PART2_EXPERIMENT = {
name: '045_ready-to-run-worfklow_v2-2',
control: 'control',
variant3: 'variant-3',
variant4: 'variant-4',
};
export const PERSONALIZED_TEMPLATES_V3 = {
name: '044_template_reco_v3',
control: 'control',
@ -861,6 +868,7 @@ export const EXPERIMENTS_TO_TRACK = [
TEMPLATE_RECO_V2.name,
READY_TO_RUN_V2_EXPERIMENT.name,
PERSONALIZED_TEMPLATES_V3.name,
READY_TO_RUN_V2_PART2_EXPERIMENT.name,
];
export const MFA_FORM = {

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { useRoute } from 'vue-router';
import { N8nCard, N8nHeading, N8nText, N8nIcon } from '@n8n/design-system';
import { useI18n } from '@n8n/i18n';
@ -21,6 +21,8 @@ const sourceControlStore = useSourceControlStore();
const projectPages = useProjectPages();
const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store();
const isLoadingReadyToRun = ref(false);
const currentUser = computed(() => usersStore.currentUser ?? ({} as IUser));
const personalProject = computed(() => projectsStore.personalProject);
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
@ -42,14 +44,20 @@ const emptyListDescription = computed(() => {
});
const showReadyToRunV2Card = computed(() => {
return readyToRunWorkflowsV2Store.getCardVisibility(
projectPermissions.value.workflow.create,
readOnlyEnv.value,
false, // loading is false in simplified layout
return (
isLoadingReadyToRun.value ||
readyToRunWorkflowsV2Store.getCardVisibility(
projectPermissions.value.workflow.create,
readOnlyEnv.value,
false, // loading is false in simplified layout
)
);
});
const handleReadyToRunV2Click = async () => {
if (isLoadingReadyToRun.value) return;
isLoadingReadyToRun.value = true;
const projectId = projectPages.isOverviewSubPage
? personalProject.value?.id
: (route.params.projectId as string);
@ -61,6 +69,7 @@ const handleReadyToRunV2Click = async () => {
projectId,
);
} catch (error) {
isLoadingReadyToRun.value = false;
toast.showError(error, i18n.baseText('generic.error'));
}
};
@ -98,17 +107,18 @@ const emit = defineEmits<{
>
<N8nCard
v-if="showReadyToRunV2Card"
:class="$style.actionCard"
hoverable
:class="[$style.actionCard, { [$style.loading]: isLoadingReadyToRun }]"
:hoverable="!isLoadingReadyToRun"
data-test-id="ready-to-run-v2-card"
@click="handleReadyToRunV2Click"
>
<div :class="$style.cardContent">
<N8nIcon
:class="$style.cardIcon"
icon="sparkles"
:icon="isLoadingReadyToRun ? 'spinner' : 'sparkles'"
color="foreground-dark"
:stroke-width="1.5"
:spin="isLoadingReadyToRun"
/>
<N8nText size="large" class="mt-xs">
{{ i18n.baseText('workflows.empty.readyToRunV2') }}
@ -201,6 +211,11 @@ const emit = defineEmits<{
color: var(--color-primary);
}
}
&.loading {
pointer-events: none;
opacity: 0.7;
}
}
.cardContent {

View File

@ -1,6 +1,6 @@
import { useTelemetry } from '@/composables/useTelemetry';
import { useToast } from '@/composables/useToast';
import { READY_TO_RUN_V2_EXPERIMENT, VIEWS } from '@/constants';
import { READY_TO_RUN_V2_PART2_EXPERIMENT, VIEWS } from '@/constants';
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useCredentialsStore } from '@/stores/credentials.store';
import { usePostHog } from '@/stores/posthog.store';
@ -15,8 +15,8 @@ import type { WorkflowDataCreate } from '@n8n/rest-api-client';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { useRouter, type RouteLocationNormalized } from 'vue-router';
import { READY_TO_RUN_WORKFLOW_V1 } from '../workflows/ai-workflow';
import { READY_TO_RUN_WORKFLOW_V2 } from '../workflows/ai-workflow-v2';
import { READY_TO_RUN_WORKFLOW_V3 } from '../workflows/ai-workflow-v3';
import { READY_TO_RUN_WORKFLOW_V4 } from '../workflows/ai-workflow-v4';
import { useEmptyStateDetection } from '../composables/useEmptyStateDetection';
const LOCAL_STORAGE_CREDENTIAL_KEY = 'N8N_READY_TO_RUN_V2_OPENAI_CREDENTIAL_ID';
@ -36,10 +36,10 @@ export const useReadyToRunWorkflowsV2Store = defineStore(
const workflowsStore = useWorkflowsStore();
const isFeatureEnabled = computed(() => {
const variant = posthogStore.getVariant(READY_TO_RUN_V2_EXPERIMENT.name);
const variant = posthogStore.getVariant(READY_TO_RUN_V2_PART2_EXPERIMENT.name);
return (
(variant === READY_TO_RUN_V2_EXPERIMENT.variant1 ||
variant === READY_TO_RUN_V2_EXPERIMENT.variant2) &&
(variant === READY_TO_RUN_V2_PART2_EXPERIMENT.variant3 ||
variant === READY_TO_RUN_V2_PART2_EXPERIMENT.variant4) &&
cloudPlanStore.userIsTrialing
);
});
@ -68,7 +68,7 @@ export const useReadyToRunWorkflowsV2Store = defineStore(
});
const getCurrentVariant = () => {
return posthogStore.getVariant(READY_TO_RUN_V2_EXPERIMENT.name);
return posthogStore.getVariant(READY_TO_RUN_V2_PART2_EXPERIMENT.name);
};
const trackExecuteAiWorkflow = (status: string) => {
@ -92,10 +92,6 @@ export const useReadyToRunWorkflowsV2Store = defineStore(
try {
const credential = await credentialsStore.claimFreeAiCredits(projectId);
if (usersStore?.currentUser?.settings) {
usersStore.currentUser.settings.userClaimedAiCredits = true;
}
claimedCredentialIdRef.value = credential.id;
telemetry.track('User claimed OpenAI credits');
@ -120,9 +116,9 @@ export const useReadyToRunWorkflowsV2Store = defineStore(
});
const workflowTemplate =
variant === READY_TO_RUN_V2_EXPERIMENT.variant2
? READY_TO_RUN_WORKFLOW_V2
: READY_TO_RUN_WORKFLOW_V1;
variant === READY_TO_RUN_V2_PART2_EXPERIMENT.variant3
? READY_TO_RUN_WORKFLOW_V3
: READY_TO_RUN_WORKFLOW_V4;
try {
let workflowToCreate: WorkflowDataCreate = {
@ -165,6 +161,10 @@ export const useReadyToRunWorkflowsV2Store = defineStore(
) => {
await claimFreeAiCredits(projectId);
await createAndOpenAiWorkflow(source, parentFolderId);
if (usersStore?.currentUser?.settings) {
usersStore.currentUser.settings.userClaimedAiCredits = true;
}
};
const getCardVisibility = (

View File

@ -0,0 +1,198 @@
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
export const READY_TO_RUN_WORKFLOW_V3: WorkflowDataCreate = {
name: 'AI Agent workflow',
meta: { templateId: 'ready-to-run-ai-workflow-v3' },
nodes: [
{
parameters: {
url: 'https://www.theverge.com/rss/index.xml',
options: {},
},
type: 'n8n-nodes-base.rssFeedReadTool',
typeVersion: 1.2,
position: [128, 448],
id: '7febc10d-90ce-4329-90fb-a9a2ca0185c4',
name: 'Get Tech News',
},
{
parameters: {
toolDescription: 'Reads the news',
url: '=https://feeds.bbci.co.uk/news/world/rss.xml',
options: {},
},
type: 'n8n-nodes-base.rssFeedReadTool',
typeVersion: 1.2,
position: [272, 448],
id: '9424428d-45e2-4085-99f6-ee223802ba5a',
name: 'Get World News',
},
{
parameters: {
model: {
__rl: true,
mode: 'list',
value: 'gpt-4.1-mini',
},
options: {},
},
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
typeVersion: 1.2,
position: [-144, 448],
id: 'a4fcf631-d3d9-4c4d-9e7b-02c93e70b23f',
name: 'OpenAI Model',
notesInFlow: true,
credentials: {},
notes: 'Free n8n credits ',
},
{
parameters: {
promptType: 'define',
text: '=Summarize world news and tech news from the last 24 hours. \nSkip your comments. \nThe titles should be "World news:" and "Tech news:" \nLimit to 10 bullet points. \nToday is {{ $today }}',
options: {},
},
type: '@n8n/n8n-nodes-langchain.agent',
typeVersion: 2.2,
position: [-144, 192],
id: '99bed296-3855-4d89-b983-f30539cfa775',
name: 'AI Summary Agent',
notesInFlow: true,
notes: 'Double-click to open',
},
{
parameters: {
content:
'### ✅ This test workflow is ready to use:\nHover over here and click the orange "Execute workflow" button below.\n',
height: 240,
width: 400,
color: 5,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-672, 112],
id: 'e664273f-63c6-4f12-804a-0fcd99c294cb',
name: 'Sticky Note2',
},
{
parameters: {
subject: 'Your news daily summary',
emailType: 'text',
message: '={{ $json.output }}',
options: {},
},
type: 'n8n-nodes-base.gmail',
typeVersion: 2.1,
position: [688, 192],
id: 'd0e843dc-c398-4d32-8c56-0bf83176add3',
name: 'Send summary with Gmail',
webhookId: '99bdd654-5c17-4ba1-b091-3d726e56f88d',
notesInFlow: true,
notes: 'Double-click to open',
},
{
parameters: {},
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [-432, 192],
id: 'e6618880-9281-4d92-91ff-c9a000429b7d',
name: 'Manual execution',
},
{
parameters: {
content:
'### Bonus (optional)\nConnect the `Output (News Summary)` to the node below, add your Google account info, and send the News summary by email.',
height: 112,
width: 384,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [544, 16],
id: '5e33fbe1-1971-48f8-81c7-08bd32e24aca',
name: 'Sticky Note',
},
{
parameters: {
assignments: {
assignments: [
{
id: '85b5c530-2c13-4424-ab83-05979bc879a5',
name: 'output',
value: '={{ $json.output }}',
type: 'string',
},
],
},
options: {},
},
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [256, 192],
id: 'f0a79856-ddcf-404b-95a7-a9bf882697ff',
name: 'Output (News summary)',
notesInFlow: true,
notes: 'Double-click to open',
},
],
connections: {
'Get Tech News': {
ai_tool: [
[
{
node: 'AI Summary Agent',
type: 'ai_tool',
index: 0,
},
],
],
},
'Get World News': {
ai_tool: [
[
{
node: 'AI Summary Agent',
type: 'ai_tool',
index: 0,
},
],
],
},
'OpenAI Model': {
ai_languageModel: [
[
{
node: 'AI Summary Agent',
type: 'ai_languageModel',
index: 0,
},
],
],
},
'AI Summary Agent': {
main: [
[
{
node: 'Output (News summary)',
type: 'main',
index: 0,
},
],
],
},
'Manual execution': {
main: [
[
{
node: 'AI Summary Agent',
type: 'main',
index: 0,
},
],
],
},
'Output (News summary)': {
main: [[]],
},
},
pinData: {},
};

View File

@ -0,0 +1,198 @@
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
export const READY_TO_RUN_WORKFLOW_V4: WorkflowDataCreate = {
name: 'AI Agent workflow',
meta: { templateId: 'ready-to-run-ai-workflow-v4' },
nodes: [
{
parameters: {
url: 'https://www.theverge.com/rss/index.xml',
options: {},
},
type: 'n8n-nodes-base.rssFeedReadTool',
typeVersion: 1.2,
position: [288, 160],
id: '6160830b-4f20-437c-b1a2-586bffe62d66',
name: 'Get Tech News',
},
{
parameters: {
toolDescription: 'Reads the news',
url: '=https://feeds.bbci.co.uk/news/world/rss.xml',
options: {},
},
type: 'n8n-nodes-base.rssFeedReadTool',
typeVersion: 1.2,
position: [416, 160],
id: '4f8ae14c-8c6a-4cf8-b51b-99af6bd23ed1',
name: 'Get World News',
},
{
parameters: {
model: {
__rl: true,
mode: 'list',
value: 'gpt-4.1-mini',
},
options: {},
},
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
typeVersion: 1.2,
position: [32, 160],
id: '95986360-8ca1-4b8a-af7e-f101e89e3654',
name: 'OpenAI Model',
notesInFlow: true,
credentials: {},
notes: 'Free n8n credits ',
},
{
parameters: {
promptType: 'define',
text: '=Summarize world news and tech news from the last 24 hours. \nSkip your comments. \nThe titles should be "World news:" and "Tech news:" \nLimit to 10 bullet points. \nToday is {{ $today }}',
options: {},
},
type: '@n8n/n8n-nodes-langchain.agent',
typeVersion: 2.2,
position: [32, -64],
id: 'd36975bc-d51f-472f-a51f-f6c745b29a8d',
name: 'AI Summary Agent',
notesInFlow: true,
notes: 'Double-click to open',
},
{
parameters: {
content:
'### ✅ This test workflow is ready to use \n\n1. Click the orange "Execute workflow" button\n\n2. Watch the workflow get the latest news and summarize it with AI \n\n3. (Bonus) Connect the `Gmail node` to the workflow to send the summary via email\n\n',
height: 256,
width: 352,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-832, -128],
id: '13abc1af-da4a-427d-8cc4-e260dff43307',
name: 'Sticky Note2',
},
{
parameters: {
content:
'[![Learn to use an AI Agent in your workflow](https://n8niostorageaccount.blob.core.windows.net/n8nio-strapi-blobs-prod/assets/thumb_2e91cdcea1.png)](https://www.youtube.com/watch?v=cMyOkQ4N-5M "Watch on YouTube")\n',
height: 208,
width: 352,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-832, 160],
id: 'e0e15104-1954-43b9-b748-0ff8441f6aeb',
name: 'Sticky Note',
},
{
parameters: {
assignments: {
assignments: [
{
id: '85b5c530-2c13-4424-ab83-05979bc879a5',
name: 'output',
value: '={{ $json.output }}',
type: 'string',
},
],
},
options: {},
},
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [464, -64],
id: 'bef94b0a-b2aa-42f6-85bb-2e23f530d799',
name: 'Output (News Summary)',
notesInFlow: true,
notes: 'Double-click to open',
},
{
parameters: {},
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [-256, -64],
id: '55cb3e43-b73c-48cb-b420-dd618de68a58',
name: 'Execute workflow',
},
{
parameters: {
subject: 'Your news daily summary',
emailType: 'text',
message: '={{ $json.output }}',
options: {},
},
type: 'n8n-nodes-base.gmail',
typeVersion: 2.1,
position: [768, -64],
id: 'e74f8dac-d766-4f4d-91f3-36604a2d4e7a',
name: 'Send summary with Gmail',
webhookId: '093b04f1-5e78-4926-9863-1b100d6f2ead',
notesInFlow: true,
notes: 'Double-click to open',
},
],
connections: {
'Get Tech News': {
ai_tool: [
[
{
node: 'AI Summary Agent',
type: 'ai_tool',
index: 0,
},
],
],
},
'Get World News': {
ai_tool: [
[
{
node: 'AI Summary Agent',
type: 'ai_tool',
index: 0,
},
],
],
},
'OpenAI Model': {
ai_languageModel: [
[
{
node: 'AI Summary Agent',
type: 'ai_languageModel',
index: 0,
},
],
],
},
'AI Summary Agent': {
main: [
[
{
node: 'Output (News Summary)',
type: 'main',
index: 0,
},
],
],
},
'Output (News Summary)': {
main: [[]],
},
'Execute workflow': {
main: [
[
{
node: 'AI Summary Agent',
type: 'main',
index: 0,
},
],
],
},
},
pinData: {},
};

View File

@ -289,7 +289,7 @@ describe('Init', () => {
expect(uiStore.pushBannerToStack).toHaveBeenCalledWith('TRIAL_OVER');
});
it('should not push TRIAL banner if trial is active but user is visiting for the first time', async () => {
it('should push TRIAL banner if trial is active', async () => {
settingsStore.settings.deployment.type = 'cloud';
usersStore.usersById = { '123': { id: '123', email: '' } as IUser };
usersStore.currentUserId = '123';
@ -297,44 +297,12 @@ describe('Init', () => {
cloudPlanStore.userIsTrialing = true;
cloudPlanStore.trialExpired = false;
// Mock localStorage to simulate first visit
const getItemSpy = vi.spyOn(Storage.prototype, 'getItem').mockReturnValue(null);
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {});
const cloudStoreSpy = vi.spyOn(cloudPlanStore, 'initialize').mockResolvedValueOnce();
await initializeAuthenticatedFeatures(false);
expect(cloudStoreSpy).toHaveBeenCalled();
expect(setItemSpy).toHaveBeenCalledWith('n8n-trial-visit-count', '1');
expect(uiStore.pushBannerToStack).not.toHaveBeenCalledWith('TRIAL');
getItemSpy.mockRestore();
setItemSpy.mockRestore();
});
it('should push TRIAL banner if trial is active and user is returning', async () => {
settingsStore.settings.deployment.type = 'cloud';
usersStore.usersById = { '123': { id: '123', email: '' } as IUser };
usersStore.currentUserId = '123';
cloudPlanStore.userIsTrialing = true;
cloudPlanStore.trialExpired = false;
// Mock localStorage to simulate return visit
const getItemSpy = vi.spyOn(Storage.prototype, 'getItem').mockReturnValue('1');
const setItemSpy = vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {});
const cloudStoreSpy = vi.spyOn(cloudPlanStore, 'initialize').mockResolvedValueOnce();
await initializeAuthenticatedFeatures(false);
expect(cloudStoreSpy).toHaveBeenCalled();
expect(setItemSpy).toHaveBeenCalledWith('n8n-trial-visit-count', '2');
expect(uiStore.pushBannerToStack).toHaveBeenCalledWith('TRIAL');
getItemSpy.mockRestore();
setItemSpy.mockRestore();
});
it('should push EMAIL_CONFIRMATION banner if user cloud info is not confirmed', async () => {

View File

@ -35,22 +35,6 @@ export const state = {
};
let authenticatedFeaturesInitialized = false;
/**
* EXP: Ready to run V2
* Tracks user visits and determines if trial banner should show
* Returns true if this is not the user's first visit
*/
function shouldShowTrialBanner(): boolean {
const VISIT_COUNT_KEY = 'n8n-trial-visit-count';
const currentCount = parseInt(localStorage.getItem(VISIT_COUNT_KEY) ?? '0', 10);
const newCount = currentCount + 1;
localStorage.setItem(VISIT_COUNT_KEY, newCount.toString());
// Don't show banner on first visit
return newCount > 1;
}
/**
* Initializes the core application stores and hooks
* This is called once, when the first route is loaded.
@ -179,7 +163,7 @@ export async function initializeAuthenticatedFeatures(
if (cloudPlanStore.userIsTrialing) {
if (cloudPlanStore.trialExpired) {
uiStore.pushBannerToStack('TRIAL_OVER');
} else if (shouldShowTrialBanner()) {
} else {
uiStore.pushBannerToStack('TRIAL');
}
} else if (cloudPlanStore.currentUserCloudInfo?.confirmed === false) {

View File

@ -32,17 +32,6 @@ const cloudTrialRequirements = {
},
};
const firstVisitRequirements: TestRequirements = {
...cloudTrialRequirements,
};
const subsequentVisitRequirements: TestRequirements = {
...cloudTrialRequirements,
storage: {
'n8n-trial-visit-count': '1',
},
};
const setupCloudTest = async (
n8n: n8nPage,
setupRequirements: (requirements: TestRequirements) => Promise<void>,
@ -54,14 +43,8 @@ const setupCloudTest = async (
test.describe('Cloud @db:reset @auth:owner', () => {
test.describe('Trial Banner', () => {
test('should not render trial banner on first visit', async ({ n8n, setupRequirements }) => {
await setupCloudTest(n8n, setupRequirements, firstVisitRequirements);
await n8n.start.fromBlankCanvas();
await expect(n8n.sideBar.getTrialBanner()).toBeHidden();
});
test('should render trial banner on subsequent visits', async ({ n8n, setupRequirements }) => {
await setupCloudTest(n8n, setupRequirements, subsequentVisitRequirements);
test('should render trial banner for opt-in cloud user', async ({ n8n, setupRequirements }) => {
await setupCloudTest(n8n, setupRequirements, cloudTrialRequirements);
await n8n.start.fromBlankCanvas();
await n8n.sideBar.expand();