fix(editor): Ready to run v2 p2 rework (no-changelog) (#22199)

This commit is contained in:
Romeo Balta 2025-11-24 08:58:59 +00:00 committed by GitHub
parent ca84de8d56
commit 019cd8ed87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 29 additions and 746 deletions

View File

@ -1,66 +0,0 @@
import type { RouteLocationNormalized } from 'vue-router';
import { useFoldersStore } from '@/features/core/folders/folders.store';
import { useProjectPages } from '@/features/collaboration/projects/composables/useProjectPages';
import { useRoute } from 'vue-router';
/**
* Determines if the instance is truly empty and should show the simplified layout
*/
export function useEmptyStateDetection() {
const foldersStore = useFoldersStore();
const projectPages = useProjectPages();
const route = useRoute();
/**
* Checks if the current state qualifies as "truly empty"
* - No workflows exist in the instance
* - User is on the main workflows view (not in a specific folder)
* - User is on overview page or personal project workflows
* - No search filters are applied
* - Not currently refreshing data
*/
const isTrulyEmpty = (currentRoute: RouteLocationNormalized = route) => {
const hasNoWorkflows = foldersStore.totalWorkflowCount === 0;
const isNotInSpecificFolder = !currentRoute.params?.folderId;
const isMainWorkflowsPage = projectPages.isOverviewSubPage || !projectPages.isSharedSubPage;
// Check for any search or filter parameters that would indicate filtering is active
const hasSearchQuery = !!currentRoute.query?.search;
const hasFilters = !!(
currentRoute.query?.status ||
currentRoute.query?.tags ||
currentRoute.query?.showArchived ||
currentRoute.query?.homeProject
);
return (
hasNoWorkflows &&
isNotInSpecificFolder &&
isMainWorkflowsPage &&
!hasSearchQuery &&
!hasFilters
);
};
/**
* Checks if we're in a state where the simplified layout should be shown
* This matches the logic from ResourcesListLayout's showEmptyState computed property
*/
const shouldShowSimplifiedLayout = (
currentRoute: RouteLocationNormalized,
isFeatureEnabled: boolean,
loading: boolean,
) => {
// Don't show simplified layout if loading or feature is disabled
if (loading || !isFeatureEnabled) {
return false;
}
return isTrulyEmpty(currentRoute);
};
return {
isTrulyEmpty,
shouldShowSimplifiedLayout,
};
}

View File

@ -1,218 +1,54 @@
import { useTelemetry } from '@/app/composables/useTelemetry';
import { useToast } from '@/app/composables/useToast';
import { READY_TO_RUN_V2_PART2_EXPERIMENT, VIEWS } from '@/app/constants';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useUsersStore } from '@/features/settings/users/users.store';
import { READY_TO_RUN_V2_PART2_EXPERIMENT } from '@/app/constants';
import { useCloudPlanStore } from '@/app/stores/cloudPlan.store';
import { usePostHog } from '@/app/stores/posthog.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useI18n } from '@n8n/i18n';
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
import { STORES } from '@n8n/stores';
import { useLocalStorage } from '@vueuse/core';
import { OPEN_AI_API_CREDENTIAL_TYPE, deepCopy } from 'n8n-workflow';
import { defineStore } from 'pinia';
import { computed, ref } from 'vue';
import { useRouter, type RouteLocationNormalized } from 'vue-router';
import { useEmptyStateDetection } from '../composables/useEmptyStateDetection';
import { computed } from 'vue';
import { READY_TO_RUN_WORKFLOW_V3 } from '../workflows/ai-workflow-v3';
import { READY_TO_RUN_WORKFLOW_V4 } from '../workflows/ai-workflow-v4';
const LOCAL_STORAGE_CREDENTIAL_KEY = 'N8N_READY_TO_RUN_V2_OPENAI_CREDENTIAL_ID';
export const useReadyToRunWorkflowsV2Store = defineStore(
STORES.EXPERIMENT_READY_TO_RUN_WORKFLOWS_V2,
() => {
const telemetry = useTelemetry();
const i18n = useI18n();
const toast = useToast();
const router = useRouter();
const credentialsStore = useCredentialsStore();
const usersStore = useUsersStore();
const settingsStore = useSettingsStore();
const posthogStore = usePostHog();
const cloudPlanStore = useCloudPlanStore();
const workflowsStore = useWorkflowsStore();
const isFeatureEnabled = computed(() => {
const variant = posthogStore.getVariant(READY_TO_RUN_V2_PART2_EXPERIMENT.name);
return (
(variant === READY_TO_RUN_V2_PART2_EXPERIMENT.variant3 ||
variant === READY_TO_RUN_V2_PART2_EXPERIMENT.variant4) &&
cloudPlanStore.userIsTrialing
);
});
const claimedCredentialIdRef = useLocalStorage(LOCAL_STORAGE_CREDENTIAL_KEY, '');
const claimingCredits = ref(false);
const userHasOpenAiCredentialAlready = computed(
() =>
!!credentialsStore.allCredentials.filter(
(credential) => credential.type === OPEN_AI_API_CREDENTIAL_TYPE,
).length,
const currentVariant = computed(() =>
posthogStore.getVariant(READY_TO_RUN_V2_PART2_EXPERIMENT.name),
);
const userHasClaimedAiCreditsAlready = computed(
() => !!usersStore.currentUser?.settings?.userClaimedAiCredits,
const isVariant3 = computed(
() => currentVariant.value === READY_TO_RUN_V2_PART2_EXPERIMENT.variant3,
);
const isVariant4 = computed(
() => currentVariant.value === READY_TO_RUN_V2_PART2_EXPERIMENT.variant4,
);
const userCanClaimOpenAiCredits = computed(() => {
return (
settingsStore.isAiCreditsEnabled &&
!userHasOpenAiCredentialAlready.value &&
!userHasClaimedAiCreditsAlready.value
);
});
const isFeatureEnabled = computed(
() => cloudPlanStore.userIsTrialing && (isVariant3.value || isVariant4.value),
);
const getCurrentVariant = () => {
return posthogStore.getVariant(READY_TO_RUN_V2_PART2_EXPERIMENT.name);
};
const trackExecuteAiWorkflow = (status: string) => {
const variant = getCurrentVariant();
telemetry.track('User executed ready to run AI workflow', {
status,
variant,
});
};
const trackExecuteAiWorkflowSuccess = () => {
const variant = getCurrentVariant();
telemetry.track('User executed ready to run AI workflow successfully', {
variant,
});
};
const claimFreeAiCredits = async (projectId?: string) => {
claimingCredits.value = true;
try {
const credential = await credentialsStore.claimFreeAiCredits(projectId);
claimedCredentialIdRef.value = credential.id;
telemetry.track('User claimed OpenAI credits');
return credential;
} catch (e) {
toast.showError(
e,
i18n.baseText('freeAi.credits.showError.claim.title'),
i18n.baseText('freeAi.credits.showError.claim.message'),
);
throw e;
} finally {
claimingCredits.value = false;
const getWorkflowForVariant = (): WorkflowDataCreate | undefined => {
if (!isFeatureEnabled.value) {
return;
}
};
const createAndOpenAiWorkflow = async (source: 'card' | 'button', parentFolderId?: string) => {
const variant = getCurrentVariant();
telemetry.track('User opened ready to run AI workflow', {
source,
variant,
});
const workflowTemplate =
variant === READY_TO_RUN_V2_PART2_EXPERIMENT.variant3
? READY_TO_RUN_WORKFLOW_V3
: READY_TO_RUN_WORKFLOW_V4;
try {
let workflowToCreate: WorkflowDataCreate = {
...workflowTemplate,
parentFolderId,
};
const credentialId = claimedCredentialIdRef.value;
if (credentialId && workflowToCreate.nodes) {
const clonedWorkflow = deepCopy(workflowToCreate);
const openAiNode = clonedWorkflow.nodes?.find((node) => node.name === 'OpenAI Model');
if (openAiNode) {
openAiNode.credentials ??= {};
openAiNode.credentials[OPEN_AI_API_CREDENTIAL_TYPE] = {
id: credentialId,
name: '',
};
}
workflowToCreate = clonedWorkflow;
}
const createdWorkflow = await workflowsStore.createNewWorkflow(workflowToCreate);
await router.push({
name: VIEWS.WORKFLOW,
params: { name: createdWorkflow.id },
});
return createdWorkflow;
} catch (error) {
toast.showError(error, i18n.baseText('generic.error'));
throw error;
if (isVariant3.value) {
return READY_TO_RUN_WORKFLOW_V3;
}
};
const claimCreditsAndOpenWorkflow = async (
source: 'card' | 'button',
parentFolderId?: string,
projectId?: string,
) => {
await claimFreeAiCredits(projectId);
await createAndOpenAiWorkflow(source, parentFolderId);
if (usersStore?.currentUser?.settings) {
usersStore.currentUser.settings.userClaimedAiCredits = true;
if (isVariant4.value) {
return READY_TO_RUN_WORKFLOW_V4;
}
};
const getCardVisibility = (
canCreate: boolean | undefined,
readOnlyEnv: boolean,
loading: boolean,
) => {
return (
!loading &&
isFeatureEnabled.value &&
userCanClaimOpenAiCredits.value &&
!readOnlyEnv &&
canCreate
);
};
const getButtonVisibility = (
hasWorkflows: boolean,
canCreate: boolean | undefined,
readOnlyEnv: boolean,
) => {
return (
isFeatureEnabled.value &&
userCanClaimOpenAiCredits.value &&
!readOnlyEnv &&
canCreate &&
hasWorkflows
);
};
const { shouldShowSimplifiedLayout } = useEmptyStateDetection();
const getSimplifiedLayoutVisibility = (route: RouteLocationNormalized, loading: boolean) => {
return shouldShowSimplifiedLayout(route, isFeatureEnabled.value, loading);
return;
};
return {
currentVariant,
getWorkflowForVariant,
isFeatureEnabled,
claimingCredits,
userCanClaimOpenAiCredits,
claimFreeAiCredits,
createAndOpenAiWorkflow,
claimCreditsAndOpenWorkflow,
getCardVisibility,
getButtonVisibility,
getSimplifiedLayoutVisibility,
trackExecuteAiWorkflow,
trackExecuteAiWorkflowSuccess,
};
},
);

View File

@ -1,253 +0,0 @@
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
export const READY_TO_RUN_WORKFLOW_V2: WorkflowDataCreate = {
name: 'AI Agent workflow',
meta: { templateId: 'ready-to-run-ai-workflow-v2' },
nodes: [
{
parameters: {
url: 'https://www.theverge.com/rss/index.xml',
options: {},
},
type: 'n8n-nodes-base.rssFeedReadTool',
typeVersion: 1.2,
position: [-16, 768],
id: '303e9b4e-cc4e-4d8a-8ede-7550f070d212',
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: [112, 768],
id: '4090a753-f131-40b1-87c3-cf74d5a7e325',
name: 'Get World News',
},
{
parameters: {
rule: {
interval: [
{
triggerAtHour: 7,
},
],
},
},
type: 'n8n-nodes-base.scheduleTrigger',
typeVersion: 1.2,
position: [-560, 752],
id: '651543b5-0213-433f-8760-57d62b8d6d64',
name: 'Run every day at 7AM',
notesInFlow: true,
notes: 'Double-click to open',
},
{
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: [160, 544],
id: '99f7bb9e-f8c0-43ca-a9a8-a76634ac9611',
name: 'Output',
notesInFlow: true,
notes: 'Double-click to open',
},
{
parameters: {},
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [-560, 544],
id: 'a0390291-6794-4673-9a6a-5c3d3a5d9e4b',
name: 'Click Execute workflow to run',
},
{
parameters: {
content: '## ⚡ Start here:',
height: 224,
width: 224,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-624, 480],
id: 'fac5929f-e065-4474-96b1-7bcc06834238',
name: 'Sticky Note',
},
{
parameters: {
model: {
__rl: true,
mode: 'list',
value: 'gpt-4.1-mini',
},
options: {},
},
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
typeVersion: 1.2,
position: [-272, 768],
id: 'b16482e8-0d48-4426-aa93-c3fee11dd3cd',
name: 'OpenAI Model',
notesInFlow: true,
credentials: {},
notes: 'Double-click to open',
},
{
parameters: {
content: '@[youtube](cMyOkQ4N-5M)',
height: 512,
width: 902,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-352, -96],
id: 'ec65e69e-77fa-4912-a4af-49e0a248e2c8',
name: 'Sticky Note3',
},
{
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:" \nToday is {{ $today }}',
options: {},
},
type: '@n8n/n8n-nodes-langchain.agent',
typeVersion: 2.2,
position: [-272, 544],
id: '084d56aa-d157-4964-9073-b36d9d9589c5',
name: 'AI Summary Agent',
notesInFlow: true,
notes: 'Double-click to open',
},
{
parameters: {
content: '### Double click here to see the results:',
height: 240,
width: 192,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [112, 464],
id: 'a4b7a69a-0db8-4b9b-a81d-fd83378043a3',
name: 'Sticky Note1',
},
{
parameters: {
content:
'### 📰 Daily AI Summary\n\n\nThis workflow gets the latest news and asks AI to summarize it for you.\n\n⭐ Bonus: Send the summary via email by connecting your Gmail account\n\n▶ Watch the video to get started ',
height: 272,
width: 224,
color: 5,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-624, 32],
id: '74d80857-5e63-47a8-8e86-8ecd10fd5f9e',
name: 'Sticky Note2',
},
{
parameters: {
subject: 'Your news daily summary',
emailType: 'text',
message: '={{ $json.output }}',
options: {},
},
type: 'n8n-nodes-base.gmail',
typeVersion: 2.1,
position: [432, 544],
id: '45625d0d-bf26-4379-9eed-7bbc8e5d87a5',
name: 'Send summary by email',
webhookId: '093b04f1-5e78-4926-9863-1b100d6f2ead',
notesInFlow: true,
credentials: {},
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,
},
],
],
},
'Run every day at 7AM': {
main: [
[
{
node: 'AI Summary Agent',
type: 'main',
index: 0,
},
],
],
},
'Click Execute workflow to run': {
main: [
[
{
node: 'AI Summary Agent',
type: 'main',
index: 0,
},
],
],
},
'OpenAI Model': {
ai_languageModel: [
[
{
node: 'AI Summary Agent',
type: 'ai_languageModel',
index: 0,
},
],
],
},
'AI Summary Agent': {
main: [
[
{
node: 'Output',
type: 'main',
index: 0,
},
],
],
},
Output: {
main: [[]],
},
},
pinData: {},
};

View File

@ -1,240 +0,0 @@
import type { WorkflowDataCreate } from '@n8n/rest-api-client';
export const READY_TO_RUN_WORKFLOW_V1: WorkflowDataCreate = {
name: 'AI Agent workflow',
meta: { templateId: 'ready-to-run-ai-workflow-v1' },
nodes: [
{
parameters: {
url: 'https://www.theverge.com/rss/index.xml',
options: {},
},
type: 'n8n-nodes-base.rssFeedReadTool',
typeVersion: 1.2,
position: [-16, 768],
id: '303e9b4e-cc4e-4d8a-8ede-7550f070d212',
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: [112, 768],
id: '4090a753-f131-40b1-87c3-cf74d5a7e325',
name: 'Get World News',
},
{
parameters: {
rule: {
interval: [
{
triggerAtHour: 7,
},
],
},
},
type: 'n8n-nodes-base.scheduleTrigger',
typeVersion: 1.2,
position: [-560, 752],
id: '651543b5-0213-433f-8760-57d62b8d6d64',
name: 'Run every day at 7AM',
notesInFlow: true,
notes: 'Double-click to open',
},
{
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: [160, 544],
id: '99f7bb9e-f8c0-43ca-a9a8-a76634ac9611',
name: 'Output',
notesInFlow: true,
notes: 'Double-click to open',
},
{
parameters: {},
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [-560, 544],
id: 'a0390291-6794-4673-9a6a-5c3d3a5d9e4b',
name: 'Click Execute workflow to run',
},
{
parameters: {
content: '## ⚡ Start here:',
height: 240,
width: 224,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-624, 464],
id: 'fac5929f-e065-4474-96b1-7bcc06834238',
name: 'Sticky Note',
},
{
parameters: {
model: {
__rl: true,
mode: 'list',
value: 'gpt-4.1-mini',
},
options: {},
},
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
typeVersion: 1.2,
position: [-272, 768],
id: 'b16482e8-0d48-4426-aa93-c3fee11dd3cd',
name: 'OpenAI Model',
notesInFlow: true,
credentials: {},
notes: 'Double-click to open',
},
{
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:" \nToday is {{ $today }}',
options: {},
},
type: '@n8n/n8n-nodes-langchain.agent',
typeVersion: 2.2,
position: [-272, 544],
id: '084d56aa-d157-4964-9073-b36d9d9589c5',
name: 'AI Summary Agent',
notesInFlow: true,
notes: 'Double-click to open',
},
{
parameters: {
content: '### Double click here to see the results:',
height: 240,
width: 192,
color: 7,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [112, 464],
id: 'a4b7a69a-0db8-4b9b-a81d-fd83378043a3',
name: 'Sticky Note1',
},
{
parameters: {
content:
'### 📰 Daily AI Summary\n\n\nThis workflow gets the latest news and asks AI to summarize it for you.\n\n⭐ Bonus: Send the summary via email by connecting your Gmail account\n\n\n\n@[youtube](cMyOkQ4N-5M)',
height: 432,
width: 384,
color: 5,
},
type: 'n8n-nodes-base.stickyNote',
typeVersion: 1,
position: [-1152, 464],
id: '74d80857-5e63-47a8-8e86-8ecd10fd5f9e',
name: 'Sticky Note2',
},
{
parameters: {
subject: 'Your news daily summary',
emailType: 'text',
message: '={{ $json.output }}',
options: {},
},
type: 'n8n-nodes-base.gmail',
typeVersion: 2.1,
position: [432, 544],
id: '45625d0d-bf26-4379-9eed-7bbc8e5d87a5',
name: 'Send summary by email',
webhookId: '093b04f1-5e78-4926-9863-1b100d6f2ead',
notesInFlow: true,
credentials: {},
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,
},
],
],
},
'Run every day at 7AM': {
main: [
[
{
node: 'AI Summary Agent',
type: 'main',
index: 0,
},
],
],
},
'Click Execute workflow to run': {
main: [
[
{
node: 'AI Summary Agent',
type: 'main',
index: 0,
},
],
],
},
'OpenAI Model': {
ai_languageModel: [
[
{
node: 'AI Summary Agent',
type: 'ai_languageModel',
index: 0,
},
],
],
},
'AI Summary Agent': {
main: [
[
{
node: 'Output',
type: 'main',
index: 0,
},
],
],
},
Output: {
main: [[]],
},
},
pinData: {},
};

View File

@ -15,6 +15,7 @@ import { VIEWS } from '@/app/constants';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useReadyToRunWorkflowsV2Store } from '@/experiments/readyToRunWorkflowsV2/stores/readyToRunWorkflowsV2.store';
const LOCAL_STORAGE_CREDENTIAL_KEY = 'N8N_READY_TO_RUN_OPENAI_CREDENTIAL_ID';
@ -27,6 +28,7 @@ export const useReadyToRunStore = defineStore(STORES.READY_TO_RUN, () => {
const usersStore = useUsersStore();
const settingsStore = useSettingsStore();
const workflowsStore = useWorkflowsStore();
const readyToRunWorkflowsV2Store = useReadyToRunWorkflowsV2Store();
const claimedCredentialIdRef = useLocalStorage(LOCAL_STORAGE_CREDENTIAL_KEY, '');
@ -83,6 +85,10 @@ export const useReadyToRunStore = defineStore(STORES.READY_TO_RUN, () => {
}
};
const getReadyToRunWorkflowTemplate = () => {
return readyToRunWorkflowsV2Store.getWorkflowForVariant() ?? READY_TO_RUN_AI_WORKFLOW;
};
const createAndOpenAiWorkflow = async (source: 'card' | 'button', parentFolderId?: string) => {
telemetry.track('User opened ready to run AI workflow', {
source,
@ -90,7 +96,7 @@ export const useReadyToRunStore = defineStore(STORES.READY_TO_RUN, () => {
try {
let workflowToCreate: WorkflowDataCreate = {
...READY_TO_RUN_AI_WORKFLOW,
...getReadyToRunWorkflowTemplate(),
parentFolderId,
};