mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-26 05:04:05 -08:00
feat(editor): Implementing the Easy AI Workflow
experiment (#12043)
This commit is contained in:
parent
7b20f8aaa8
commit
67ed1d2c3c
|
@ -98,6 +98,10 @@ describe('Workflow Selector Parameter', () => {
|
||||||
|
|
||||||
getVisiblePopper().findChildByTestId('rlc-item').eq(0).click();
|
getVisiblePopper().findChildByTestId('rlc-item').eq(0).click();
|
||||||
|
|
||||||
cy.get('@windowOpen').should('be.calledWith', '/workflows/onboarding/0?sampleSubWorkflows=0');
|
const SAMPLE_SUBWORKFLOW_TEMPLATE_ID = 'VMiAxXa3lCAizGB5f7dVZQSFfg3FtHkdTKvLuupqBls=';
|
||||||
|
cy.get('@windowOpen').should(
|
||||||
|
'be.calledWith',
|
||||||
|
`/workflows/onboarding/${SAMPLE_SUBWORKFLOW_TEMPLATE_ID}?sampleSubWorkflows=0`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,4 +4,5 @@ import { Z } from 'zod-class';
|
||||||
export class SettingsUpdateRequestDto extends Z.class({
|
export class SettingsUpdateRequestDto extends Z.class({
|
||||||
userActivated: z.boolean().optional(),
|
userActivated: z.boolean().optional(),
|
||||||
allowSSOManualLogin: z.boolean().optional(),
|
allowSSOManualLogin: z.boolean().optional(),
|
||||||
|
easyAIWorkflowOnboarded: z.boolean().optional(),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
|
@ -173,4 +173,5 @@ export interface FrontendSettings {
|
||||||
};
|
};
|
||||||
betaFeatures: FrontendBetaFeatures[];
|
betaFeatures: FrontendBetaFeatures[];
|
||||||
virtualSchemaView: boolean;
|
virtualSchemaView: boolean;
|
||||||
|
easyAIWorkflowOnboarded: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,7 @@ type ExceptionPaths = {
|
||||||
processedDataManager: IProcessedDataConfig;
|
processedDataManager: IProcessedDataConfig;
|
||||||
'userManagement.isInstanceOwnerSetUp': boolean;
|
'userManagement.isInstanceOwnerSetUp': boolean;
|
||||||
'ui.banners.dismissed': string[] | undefined;
|
'ui.banners.dismissed': string[] | undefined;
|
||||||
|
easyAIWorkflowOnboarded: boolean | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
// -----------------------------------
|
// -----------------------------------
|
||||||
|
|
|
@ -232,6 +232,7 @@ export class FrontendService {
|
||||||
},
|
},
|
||||||
betaFeatures: this.frontendConfig.betaFeatures,
|
betaFeatures: this.frontendConfig.betaFeatures,
|
||||||
virtualSchemaView: config.getEnv('virtualSchemaView'),
|
virtualSchemaView: config.getEnv('virtualSchemaView'),
|
||||||
|
easyAIWorkflowOnboarded: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,6 +275,11 @@ export class FrontendService {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.settings.banners.dismissed = dismissedBanners;
|
this.settings.banners.dismissed = dismissedBanners;
|
||||||
|
try {
|
||||||
|
this.settings.easyAIWorkflowOnboarded = config.getEnv('easyAIWorkflowOnboarded') ?? false;
|
||||||
|
} catch {
|
||||||
|
this.settings.easyAIWorkflowOnboarded = false;
|
||||||
|
}
|
||||||
|
|
||||||
const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
|
const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
|
||||||
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
|
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
|
||||||
|
|
|
@ -249,6 +249,16 @@ export interface IWorkflowDataCreate extends IWorkflowDataUpdate {
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workflow data with mandatory `templateId`
|
||||||
|
* This is used to identify sample workflows that we create for onboarding
|
||||||
|
*/
|
||||||
|
export interface WorkflowDataWithTemplateId extends Omit<IWorkflowDataCreate, 'meta'> {
|
||||||
|
meta: WorkflowMetadata & {
|
||||||
|
templateId: Required<WorkflowMetadata>['templateId'];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface IWorkflowToShare extends IWorkflowDataUpdate {
|
export interface IWorkflowToShare extends IWorkflowDataUpdate {
|
||||||
meta: WorkflowMetadata;
|
meta: WorkflowMetadata;
|
||||||
}
|
}
|
||||||
|
|
|
@ -127,4 +127,5 @@ export const defaultSettings: FrontendSettings = {
|
||||||
},
|
},
|
||||||
betaFeatures: [],
|
betaFeatures: [],
|
||||||
virtualSchemaView: false,
|
virtualSchemaView: false,
|
||||||
|
easyAIWorkflowOnboarded: false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -79,7 +79,6 @@ import {
|
||||||
REPORTED_SOURCE_OTHER,
|
REPORTED_SOURCE_OTHER,
|
||||||
REPORTED_SOURCE_OTHER_KEY,
|
REPORTED_SOURCE_OTHER_KEY,
|
||||||
VIEWS,
|
VIEWS,
|
||||||
MORE_ONBOARDING_OPTIONS_EXPERIMENT,
|
|
||||||
COMMUNITY_PLUS_ENROLLMENT_MODAL,
|
COMMUNITY_PLUS_ENROLLMENT_MODAL,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
|
@ -552,12 +551,9 @@ const onSave = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeCallback = () => {
|
const closeCallback = () => {
|
||||||
const isPartOfOnboardingExperiment =
|
|
||||||
posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) ===
|
|
||||||
MORE_ONBOARDING_OPTIONS_EXPERIMENT.control;
|
|
||||||
// In case the redirect to homepage for new users didn't happen
|
// In case the redirect to homepage for new users didn't happen
|
||||||
// we try again after closing the modal
|
// we try again after closing the modal
|
||||||
if (route.name !== VIEWS.HOMEPAGE && !isPartOfOnboardingExperiment) {
|
if (route.name !== VIEWS.HOMEPAGE) {
|
||||||
void router.replace({ name: VIEWS.HOMEPAGE });
|
void router.replace({ name: VIEWS.HOMEPAGE });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,8 @@ import { useWorkflowResourceLocatorModes } from './useWorkflowResourceLocatorMod
|
||||||
import { useWorkflowResourcesLocator } from './useWorkflowResourcesLocator';
|
import { useWorkflowResourcesLocator } from './useWorkflowResourcesLocator';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL, SAMPLE_SUBWORKFLOW_WORKFLOW_ID } from '@/constants';
|
import { NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL } from '@/constants';
|
||||||
|
import { SAMPLE_SUBWORKFLOW_WORKFLOW } from '@/constants.workflows';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: INodeParameterResourceLocator;
|
modelValue: INodeParameterResourceLocator;
|
||||||
|
@ -231,7 +232,7 @@ const onAddResourceClicked = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
window.open(
|
window.open(
|
||||||
`/workflows/onboarding/${SAMPLE_SUBWORKFLOW_WORKFLOW_ID}?${urlSearchParams.toString()}`,
|
`/workflows/onboarding/${SAMPLE_SUBWORKFLOW_WORKFLOW.meta.templateId}?${urlSearchParams.toString()}`,
|
||||||
'_blank',
|
'_blank',
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { computed, useSlots } from 'vue';
|
import { computed, useSlots } from 'vue';
|
||||||
import type { BannerName } from 'n8n-workflow';
|
import type { BannerName } from 'n8n-workflow';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: BannerName;
|
name: BannerName;
|
||||||
|
@ -10,6 +11,8 @@ interface Props {
|
||||||
dismissible?: boolean;
|
dismissible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
|
|
||||||
|
@ -51,7 +54,7 @@ async function onCloseClick() {
|
||||||
v-if="dismissible"
|
v-if="dismissible"
|
||||||
size="small"
|
size="small"
|
||||||
icon="times"
|
icon="times"
|
||||||
title="Dismiss"
|
:title="i18n.baseText('generic.dismiss')"
|
||||||
class="clickable"
|
class="clickable"
|
||||||
:data-test-id="`banner-${props.name}-close`"
|
:data-test-id="`banner-${props.name}-close`"
|
||||||
@click="onCloseClick"
|
@click="onCloseClick"
|
||||||
|
|
|
@ -36,6 +36,7 @@ import type { PushMessageQueueItem } from '@/types';
|
||||||
import { useAssistantStore } from '@/stores/assistant.store';
|
import { useAssistantStore } from '@/stores/assistant.store';
|
||||||
import NodeExecutionErrorMessage from '@/components/NodeExecutionErrorMessage.vue';
|
import NodeExecutionErrorMessage from '@/components/NodeExecutionErrorMessage.vue';
|
||||||
import type { IExecutionResponse } from '@/Interface';
|
import type { IExecutionResponse } from '@/Interface';
|
||||||
|
import { EASY_AI_WORKFLOW_JSON } from '@/constants.workflows';
|
||||||
|
|
||||||
export function usePushConnection({ router }: { router: ReturnType<typeof useRouter> }) {
|
export function usePushConnection({ router }: { router: ReturnType<typeof useRouter> }) {
|
||||||
const workflowHelpers = useWorkflowHelpers({ router });
|
const workflowHelpers = useWorkflowHelpers({ router });
|
||||||
|
@ -199,6 +200,23 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (receivedData.type === 'executionFinished') {
|
||||||
|
const workflow = workflowsStore.getWorkflowById(receivedData.data.workflowId);
|
||||||
|
if (workflow?.meta?.templateId) {
|
||||||
|
const isEasyAIWorkflow =
|
||||||
|
workflow.meta.templateId === EASY_AI_WORKFLOW_JSON.meta.templateId;
|
||||||
|
if (isEasyAIWorkflow) {
|
||||||
|
telemetry.track(
|
||||||
|
'User executed test AI workflow',
|
||||||
|
{
|
||||||
|
status: receivedData.data.status,
|
||||||
|
},
|
||||||
|
{ withPostHog: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { executionId } = receivedData.data;
|
const { executionId } = receivedData.data;
|
||||||
const { activeExecutionId } = workflowsStore;
|
const { activeExecutionId } = workflowsStore;
|
||||||
if (executionId !== activeExecutionId) {
|
if (executionId !== activeExecutionId) {
|
||||||
|
|
|
@ -1181,6 +1181,14 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
||||||
tagsStore.upsertTags(tags);
|
tagsStore.upsertTags(tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if workflow contains any node from specified package
|
||||||
|
* by performing a quick check based on the node type name.
|
||||||
|
*/
|
||||||
|
const containsNodeFromPackage = (workflow: IWorkflowDb, packageName: string) => {
|
||||||
|
return workflow.nodes.some((node) => node.type.startsWith(packageName));
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setDocumentTitle,
|
setDocumentTitle,
|
||||||
resolveParameter,
|
resolveParameter,
|
||||||
|
@ -1207,5 +1215,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
||||||
promptSaveUnsavedWorkflowChanges,
|
promptSaveUnsavedWorkflowChanges,
|
||||||
initState,
|
initState,
|
||||||
getNodeParametersWithResolvedExpressions,
|
getNodeParametersWithResolvedExpressions,
|
||||||
|
containsNodeFromPackage,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import type {
|
import type {
|
||||||
EnterpriseEditionFeatureKey,
|
EnterpriseEditionFeatureKey,
|
||||||
EnterpriseEditionFeatureValue,
|
EnterpriseEditionFeatureValue,
|
||||||
INodeUi,
|
|
||||||
IWorkflowDataCreate,
|
|
||||||
NodeCreatorOpenSource,
|
NodeCreatorOpenSource,
|
||||||
} from './Interface';
|
} from './Interface';
|
||||||
import { NodeConnectionType } from 'n8n-workflow';
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
@ -696,23 +694,24 @@ export const AI_ASSISTANT_EXPERIMENT = {
|
||||||
variant: 'variant',
|
variant: 'variant',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MORE_ONBOARDING_OPTIONS_EXPERIMENT = {
|
|
||||||
name: '022_more_onboarding_options',
|
|
||||||
control: 'control',
|
|
||||||
variant: 'variant',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CREDENTIAL_DOCS_EXPERIMENT = {
|
export const CREDENTIAL_DOCS_EXPERIMENT = {
|
||||||
name: '024_credential_docs',
|
name: '024_credential_docs',
|
||||||
control: 'control',
|
control: 'control',
|
||||||
variant: 'variant',
|
variant: 'variant',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EASY_AI_WORKFLOW_EXPERIMENT = {
|
||||||
|
name: '026_easy_ai_workflow',
|
||||||
|
control: 'control',
|
||||||
|
variant: 'variant',
|
||||||
|
};
|
||||||
|
|
||||||
export const EXPERIMENTS_TO_TRACK = [
|
export const EXPERIMENTS_TO_TRACK = [
|
||||||
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
|
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
|
||||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name,
|
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name,
|
||||||
AI_ASSISTANT_EXPERIMENT.name,
|
AI_ASSISTANT_EXPERIMENT.name,
|
||||||
MORE_ONBOARDING_OPTIONS_EXPERIMENT.name,
|
|
||||||
CREDENTIAL_DOCS_EXPERIMENT.name,
|
CREDENTIAL_DOCS_EXPERIMENT.name,
|
||||||
|
EASY_AI_WORKFLOW_EXPERIMENT.name,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const WORKFLOW_EVALUATION_EXPERIMENT = '025_workflow_evaluation';
|
export const WORKFLOW_EVALUATION_EXPERIMENT = '025_workflow_evaluation';
|
||||||
|
@ -893,43 +892,6 @@ export const BROWSER_ID_STORAGE_KEY = 'n8n-browserId';
|
||||||
|
|
||||||
export const APP_MODALS_ELEMENT_ID = 'app-modals';
|
export const APP_MODALS_ELEMENT_ID = 'app-modals';
|
||||||
|
|
||||||
export const SAMPLE_SUBWORKFLOW_WORKFLOW_ID = '0';
|
|
||||||
|
|
||||||
export const NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL = 'new-sample-sub-workflow-created';
|
export const NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL = 'new-sample-sub-workflow-created';
|
||||||
|
|
||||||
export const SAMPLE_SUBWORKFLOW_WORKFLOW: IWorkflowDataCreate = {
|
export const AI_NODES_PACKAGE_NAME = '@n8n/n8n-nodes-langchain';
|
||||||
name: 'My Sub-Workflow',
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
parameters: {},
|
|
||||||
id: 'c055762a-8fe7-4141-a639-df2372f30060',
|
|
||||||
name: 'Execute Workflow Trigger',
|
|
||||||
type: 'n8n-nodes-base.executeWorkflowTrigger',
|
|
||||||
position: [260, 340],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
parameters: {},
|
|
||||||
id: 'b5942df6-0160-4ef7-965d-57583acdc8aa',
|
|
||||||
name: 'Replace me with your logic',
|
|
||||||
type: 'n8n-nodes-base.noOp',
|
|
||||||
position: [520, 340],
|
|
||||||
},
|
|
||||||
] as INodeUi[],
|
|
||||||
connections: {
|
|
||||||
'Execute Workflow Trigger': {
|
|
||||||
main: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
node: 'Replace me with your logic',
|
|
||||||
type: NodeConnectionType.Main,
|
|
||||||
index: 0,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
executionOrder: 'v1',
|
|
||||||
},
|
|
||||||
pinData: {},
|
|
||||||
};
|
|
||||||
|
|
208
packages/editor-ui/src/constants.workflows.ts
Normal file
208
packages/editor-ui/src/constants.workflows.ts
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
import { NodeConnectionType } from 'n8n-workflow';
|
||||||
|
import type { INodeUi, WorkflowDataWithTemplateId } from './Interface';
|
||||||
|
|
||||||
|
export const EASY_AI_WORKFLOW_JSON: WorkflowDataWithTemplateId = {
|
||||||
|
name: 'Demo: My first AI Agent in n8n',
|
||||||
|
meta: {
|
||||||
|
templateId: 'PT1i+zU92Ii5O2XCObkhfHJR5h9rNJTpiCIkYJk9jHU=',
|
||||||
|
},
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: '0d7e4666-bc0e-489a-9e8f-a5ef191f4954',
|
||||||
|
name: 'Google Calendar',
|
||||||
|
type: 'n8n-nodes-base.googleCalendarTool',
|
||||||
|
typeVersion: 1.2,
|
||||||
|
position: [880, 220],
|
||||||
|
parameters: {
|
||||||
|
operation: 'getAll',
|
||||||
|
calendar: {
|
||||||
|
__rl: true,
|
||||||
|
mode: 'list',
|
||||||
|
},
|
||||||
|
returnAll: true,
|
||||||
|
options: {
|
||||||
|
timeMin:
|
||||||
|
"={{ $fromAI('after', 'The earliest datetime we want to look for events for') }}",
|
||||||
|
timeMax: "={{ $fromAI('before', 'The latest datetime we want to look for events for') }}",
|
||||||
|
query:
|
||||||
|
"={{ $fromAI('query', 'The search query to look for in the calendar. Leave empty if no search query is needed') }}",
|
||||||
|
singleEvents: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5b410409-5b0b-47bd-b413-5b9b1000a063',
|
||||||
|
name: 'When chat message received',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.chatTrigger',
|
||||||
|
typeVersion: 1.1,
|
||||||
|
position: [360, 20],
|
||||||
|
webhookId: 'a889d2ae-2159-402f-b326-5f61e90f602e',
|
||||||
|
parameters: {
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '29963449-1dc1-487d-96f2-7ff0a5c3cd97',
|
||||||
|
name: 'AI Agent',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.agent',
|
||||||
|
typeVersion: 1.7,
|
||||||
|
position: [560, 20],
|
||||||
|
parameters: {
|
||||||
|
options: {
|
||||||
|
systemMessage:
|
||||||
|
"=You're a helpful assistant that the user to answer questions about their calendar.\n\nToday is {{ $now.format('cccc') }} the {{ $now.format('yyyy-MM-dd HH:mm') }}.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'eae35513-07c2-4de2-a795-a153b6934c1b',
|
||||||
|
name: 'Sticky Note',
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [0, 0],
|
||||||
|
parameters: {
|
||||||
|
content:
|
||||||
|
'## 👋 Welcome to n8n!\nThis example shows how to build an AI Agent that interacts with your \ncalendar.\n\n### 1. Connect your accounts\n- Set up your [OpenAI credentials](https://docs.n8n.io/integrations/builtin/credentials/openai/?utm_source=n8n_app&utm_medium=credential_settings&utm_campaign=create_new_credentials_modal) in the `OpenAI Model` node\n- Connect your Google account in the `Google Calendar` node credentials section\n\n### 2. Ready to test it?\nClick Chat below and start asking questions! For example you can try `What meetings do I have today?`',
|
||||||
|
height: 389,
|
||||||
|
width: 319,
|
||||||
|
color: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '68b59889-7aca-49fd-a49b-d86fa6239b96',
|
||||||
|
name: 'Sticky Note1',
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [820, 200],
|
||||||
|
parameters: {
|
||||||
|
content:
|
||||||
|
"\n\n\n\n\n\n\n\n\n\n\n\nDon't have **Google Calendar**? Simply exchange this with the **Microsoft Outlook** or other tools",
|
||||||
|
height: 253,
|
||||||
|
width: 226,
|
||||||
|
color: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cbaedf86-9153-4778-b893-a7e50d3e04ba',
|
||||||
|
name: 'OpenAI Model',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [520, 220],
|
||||||
|
parameters: {
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '75481370-bade-4d90-a878-3a3b0201edcc',
|
||||||
|
name: 'Memory',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
|
||||||
|
typeVersion: 1.3,
|
||||||
|
position: [680, 220],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '907552eb-6e0f-472e-9d90-4513a67a31db',
|
||||||
|
name: 'Sticky Note3',
|
||||||
|
type: 'n8n-nodes-base.stickyNote',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [0, 400],
|
||||||
|
parameters: {
|
||||||
|
content:
|
||||||
|
'### Want to learn more?\nWant to learn more about AI and how to apply it best in n8n? Have a look at our [new tutorial series on YouTube](https://www.youtube.com/watch?v=yzvLfHb0nqE&lc).',
|
||||||
|
height: 100,
|
||||||
|
width: 317,
|
||||||
|
color: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as INodeUi[],
|
||||||
|
connections: {
|
||||||
|
'Google Calendar': {
|
||||||
|
ai_tool: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Agent',
|
||||||
|
type: NodeConnectionType.AiTool,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'When chat message received': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Agent',
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'OpenAI Model': {
|
||||||
|
ai_languageModel: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Agent',
|
||||||
|
type: NodeConnectionType.AiLanguageModel,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Memory: {
|
||||||
|
ai_memory: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'AI Agent',
|
||||||
|
type: NodeConnectionType.AiMemory,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
executionOrder: 'v1',
|
||||||
|
},
|
||||||
|
pinData: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SAMPLE_SUBWORKFLOW_WORKFLOW: WorkflowDataWithTemplateId = {
|
||||||
|
name: 'My Sub-Workflow',
|
||||||
|
meta: {
|
||||||
|
templateId: 'VMiAxXa3lCAizGB5f7dVZQSFfg3FtHkdTKvLuupqBls=',
|
||||||
|
},
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'c055762a-8fe7-4141-a639-df2372f30060',
|
||||||
|
name: 'Execute Workflow Trigger',
|
||||||
|
type: 'n8n-nodes-base.executeWorkflowTrigger',
|
||||||
|
position: [260, 340],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b5942df6-0160-4ef7-965d-57583acdc8aa',
|
||||||
|
name: 'Replace me with your logic',
|
||||||
|
type: 'n8n-nodes-base.noOp',
|
||||||
|
position: [520, 340],
|
||||||
|
parameters: {},
|
||||||
|
},
|
||||||
|
] as INodeUi[],
|
||||||
|
connections: {
|
||||||
|
'Execute Workflow Trigger': {
|
||||||
|
main: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
node: 'Replace me with your logic',
|
||||||
|
type: NodeConnectionType.Main,
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
executionOrder: 'v1',
|
||||||
|
},
|
||||||
|
pinData: {},
|
||||||
|
};
|
|
@ -59,6 +59,8 @@
|
||||||
"generic.error": "Something went wrong",
|
"generic.error": "Something went wrong",
|
||||||
"generic.settings": "Settings",
|
"generic.settings": "Settings",
|
||||||
"generic.service": "the service",
|
"generic.service": "the service",
|
||||||
|
"generic.tryNow": "Try now",
|
||||||
|
"generic.dismiss": "Dismiss",
|
||||||
"generic.unsavedWork.confirmMessage.headline": "Save changes before leaving?",
|
"generic.unsavedWork.confirmMessage.headline": "Save changes before leaving?",
|
||||||
"generic.unsavedWork.confirmMessage.message": "If you don't save, you will lose your changes.",
|
"generic.unsavedWork.confirmMessage.message": "If you don't save, you will lose your changes.",
|
||||||
"generic.unsavedWork.confirmMessage.confirmButtonText": "Save",
|
"generic.unsavedWork.confirmMessage.confirmButtonText": "Save",
|
||||||
|
@ -2294,6 +2296,8 @@
|
||||||
"workflows.empty.browseTemplates": "Explore workflow templates",
|
"workflows.empty.browseTemplates": "Explore workflow templates",
|
||||||
"workflows.empty.learnN8n": "Learn n8n",
|
"workflows.empty.learnN8n": "Learn n8n",
|
||||||
"workflows.empty.button.disabled.tooltip": "Your current role in the project does not allow you to create workflows",
|
"workflows.empty.button.disabled.tooltip": "Your current role in the project does not allow you to create workflows",
|
||||||
|
"workflows.empty.easyAI": "Test a ready-to-go AI Agent example",
|
||||||
|
"workflows.list.easyAI": "Test the power of AI in n8n with this ready-to-go AI Agent Workflow",
|
||||||
"workflows.shareModal.title": "Share '{name}'",
|
"workflows.shareModal.title": "Share '{name}'",
|
||||||
"workflows.shareModal.title.static": "Shared with {projectName}",
|
"workflows.shareModal.title.static": "Shared with {projectName}",
|
||||||
"workflows.shareModal.select.placeholder": "Add users...",
|
"workflows.shareModal.select.placeholder": "Add users...",
|
||||||
|
|
|
@ -74,6 +74,16 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
|
||||||
|
|
||||||
const globalRoleName = computed(() => currentUser.value?.role ?? 'default');
|
const globalRoleName = computed(() => currentUser.value?.role ?? 'default');
|
||||||
|
|
||||||
|
const isEasyAIWorkflowOnboardingDone = computed(() =>
|
||||||
|
Boolean(currentUser.value?.settings?.easyAIWorkflowOnboarded),
|
||||||
|
);
|
||||||
|
|
||||||
|
const setEasyAIWorkflowOnboardingDone = () => {
|
||||||
|
if (currentUser.value?.settings) {
|
||||||
|
currentUser.value.settings.easyAIWorkflowOnboarded = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const personalizedNodeTypes = computed(() => {
|
const personalizedNodeTypes = computed(() => {
|
||||||
const user = currentUser.value;
|
const user = currentUser.value;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -410,5 +420,7 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
|
||||||
sendConfirmationEmail,
|
sendConfirmationEmail,
|
||||||
updateGlobalRole,
|
updateGlobalRole,
|
||||||
reset,
|
reset,
|
||||||
|
isEasyAIWorkflowOnboardingDone,
|
||||||
|
setEasyAIWorkflowOnboardingDone,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
AI_NODES_PACKAGE_NAME,
|
||||||
CHAT_TRIGGER_NODE_TYPE,
|
CHAT_TRIGGER_NODE_TYPE,
|
||||||
DEFAULT_NEW_WORKFLOW_NAME,
|
DEFAULT_NEW_WORKFLOW_NAME,
|
||||||
DUPLICATE_POSTFFIX,
|
DUPLICATE_POSTFFIX,
|
||||||
|
@ -86,6 +87,8 @@ import { useRouter } from 'vue-router';
|
||||||
import { useSettingsStore } from './settings.store';
|
import { useSettingsStore } from './settings.store';
|
||||||
import { closeFormPopupWindow, openFormPopupWindow } from '@/utils/executionUtils';
|
import { closeFormPopupWindow, openFormPopupWindow } from '@/utils/executionUtils';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import { updateCurrentUserSettings } from '@/api/users';
|
||||||
|
|
||||||
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
|
const defaults: Omit<IWorkflowDb, 'id'> & { settings: NonNullable<IWorkflowDb['settings']> } = {
|
||||||
name: '',
|
name: '',
|
||||||
|
@ -119,6 +122,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
|
const usersStore = useUsersStore();
|
||||||
|
|
||||||
// -1 means the backend chooses the default
|
// -1 means the backend chooses the default
|
||||||
// 0 is the old flow
|
// 0 is the old flow
|
||||||
|
@ -1415,12 +1419,25 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
(sendData as unknown as IDataObject).projectId = projectStore.currentProjectId;
|
(sendData as unknown as IDataObject).projectId = projectStore.currentProjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await makeRestApiRequest(
|
const newWorkflow = await makeRestApiRequest<IWorkflowDb>(
|
||||||
rootStore.restApiContext,
|
rootStore.restApiContext,
|
||||||
'POST',
|
'POST',
|
||||||
'/workflows',
|
'/workflows',
|
||||||
sendData as unknown as IDataObject,
|
sendData as unknown as IDataObject,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isAIWorkflow = workflowHelpers.containsNodeFromPackage(
|
||||||
|
newWorkflow,
|
||||||
|
AI_NODES_PACKAGE_NAME,
|
||||||
|
);
|
||||||
|
if (isAIWorkflow && !usersStore.isEasyAIWorkflowOnboardingDone) {
|
||||||
|
await updateCurrentUserSettings(rootStore.restApiContext, {
|
||||||
|
easyAIWorkflowOnboarded: true,
|
||||||
|
});
|
||||||
|
usersStore.setEasyAIWorkflowOnboardingDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
return newWorkflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateWorkflow(
|
async function updateWorkflow(
|
||||||
|
@ -1432,12 +1449,24 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
data.settings = undefined;
|
data.settings = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await makeRestApiRequest(
|
const updatedWorkflow = await makeRestApiRequest<IWorkflowDb>(
|
||||||
rootStore.restApiContext,
|
rootStore.restApiContext,
|
||||||
'PATCH',
|
'PATCH',
|
||||||
`/workflows/${id}${forceSave ? '?forceSave=true' : ''}`,
|
`/workflows/${id}${forceSave ? '?forceSave=true' : ''}`,
|
||||||
data as unknown as IDataObject,
|
data as unknown as IDataObject,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
workflowHelpers.containsNodeFromPackage(updatedWorkflow, AI_NODES_PACKAGE_NAME) &&
|
||||||
|
!usersStore.isEasyAIWorkflowOnboardingDone
|
||||||
|
) {
|
||||||
|
await updateCurrentUserSettings(rootStore.restApiContext, {
|
||||||
|
easyAIWorkflowOnboarded: true,
|
||||||
|
});
|
||||||
|
usersStore.setEasyAIWorkflowOnboardingDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedWorkflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runWorkflow(startRunData: IStartRunData): Promise<IExecutionPushResponse> {
|
async function runWorkflow(startRunData: IStartRunData): Promise<IExecutionPushResponse> {
|
||||||
|
|
|
@ -5,17 +5,15 @@ import { useRouter } from 'vue-router';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
|
||||||
import type { IFormBoxConfig } from '@/Interface';
|
import type { IFormBoxConfig } from '@/Interface';
|
||||||
import { MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
|
|
||||||
import AuthView from '@/views/AuthView.vue';
|
import AuthView from '@/views/AuthView.vue';
|
||||||
|
|
||||||
const posthogStore = usePostHog();
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const usersStore = useUsersStore();
|
const usersStore = useUsersStore();
|
||||||
|
@ -85,9 +83,6 @@ const formConfig: IFormBoxConfig = reactive({
|
||||||
const onSubmit = async (values: { [key: string]: string | boolean }) => {
|
const onSubmit = async (values: { [key: string]: string | boolean }) => {
|
||||||
try {
|
try {
|
||||||
const forceRedirectedHere = settingsStore.showSetupPage;
|
const forceRedirectedHere = settingsStore.showSetupPage;
|
||||||
const isPartOfOnboardingExperiment =
|
|
||||||
posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) ===
|
|
||||||
MORE_ONBOARDING_OPTIONS_EXPERIMENT.variant;
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await usersStore.createOwner(
|
await usersStore.createOwner(
|
||||||
values as { firstName: string; lastName: string; email: string; password: string },
|
values as { firstName: string; lastName: string; email: string; password: string },
|
||||||
|
@ -98,13 +93,8 @@ const onSubmit = async (values: { [key: string]: string | boolean }) => {
|
||||||
await uiStore.submitContactEmail(values.email.toString(), values.agree);
|
await uiStore.submitContactEmail(values.email.toString(), values.agree);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (forceRedirectedHere) {
|
if (forceRedirectedHere) {
|
||||||
if (isPartOfOnboardingExperiment) {
|
await router.push({ name: VIEWS.HOMEPAGE });
|
||||||
await router.push({ name: VIEWS.WORKFLOWS });
|
|
||||||
} else {
|
|
||||||
await router.push({ name: VIEWS.HOMEPAGE });
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
await router.push({ name: VIEWS.USERS_SETTINGS });
|
await router.push({ name: VIEWS.USERS_SETTINGS });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLoadingService } from '@/composables/useLoadingService';
|
import { useLoadingService } from '@/composables/useLoadingService';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import {
|
import { NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL, VIEWS } from '@/constants';
|
||||||
NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL,
|
|
||||||
SAMPLE_SUBWORKFLOW_WORKFLOW,
|
|
||||||
SAMPLE_SUBWORKFLOW_WORKFLOW_ID,
|
|
||||||
VIEWS,
|
|
||||||
} from '@/constants';
|
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
import { useTemplatesStore } from '@/stores/templates.store';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { onMounted } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import type { IWorkflowDataCreate } from '@/Interface';
|
import type { IWorkflowDataCreate } from '@/Interface';
|
||||||
|
import { EASY_AI_WORKFLOW_JSON, SAMPLE_SUBWORKFLOW_WORKFLOW } from '@/constants.workflows';
|
||||||
|
|
||||||
const loadingService = useLoadingService();
|
const loadingService = useLoadingService();
|
||||||
const templateStore = useTemplatesStore();
|
const templateStore = useTemplatesStore();
|
||||||
|
@ -21,10 +17,14 @@ const route = useRoute();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const openWorkflowTemplate = async (templateId: string) => {
|
const openWorkflowTemplate = async (templateId: string) => {
|
||||||
if (templateId === SAMPLE_SUBWORKFLOW_WORKFLOW_ID) {
|
if (templateId === SAMPLE_SUBWORKFLOW_WORKFLOW.meta.templateId) {
|
||||||
await openSampleSubworkflow();
|
await openSampleSubworkflow();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (templateId === EASY_AI_WORKFLOW_JSON.meta.templateId) {
|
||||||
|
await openEasyAIWorkflow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loadingService.startLoading();
|
loadingService.startLoading();
|
||||||
|
@ -63,6 +63,21 @@ const openWorkflowTemplate = async (templateId: string) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openEasyAIWorkflow = async () => {
|
||||||
|
try {
|
||||||
|
loadingService.startLoading();
|
||||||
|
const newWorkflow = await workflowsStore.createNewWorkflow(EASY_AI_WORKFLOW_JSON);
|
||||||
|
await router.replace({
|
||||||
|
name: VIEWS.WORKFLOW,
|
||||||
|
params: { name: newWorkflow.id },
|
||||||
|
});
|
||||||
|
loadingService.stopLoading();
|
||||||
|
} catch (e) {
|
||||||
|
await router.replace({ name: VIEWS.NEW_WORKFLOW });
|
||||||
|
loadingService.stopLoading();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openSampleSubworkflow = async () => {
|
const openSampleSubworkflow = async () => {
|
||||||
try {
|
try {
|
||||||
loadingService.startLoading();
|
loadingService.startLoading();
|
||||||
|
|
|
@ -5,10 +5,9 @@ import { useUsersStore } from '@/stores/users.store';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { STORES, MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants';
|
import { STORES, VIEWS } from '@/constants';
|
||||||
import { mockedStore } from '@/__tests__/utils';
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import type { IUser, IWorkflowDb } from '@/Interface';
|
||||||
import type { Cloud, IUser, IWorkflowDb } from '@/Interface';
|
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import type { Project } from '@/types/projects.types';
|
import type { Project } from '@/types/projects.types';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
@ -73,9 +72,7 @@ describe('WorkflowsView', () => {
|
||||||
|
|
||||||
describe('when onboardingExperiment -> False', () => {
|
describe('when onboardingExperiment -> False', () => {
|
||||||
const pinia = createTestingPinia({ initialState });
|
const pinia = createTestingPinia({ initialState });
|
||||||
const posthog = mockedStore(usePostHog);
|
|
||||||
const sourceControl = mockedStore(useSourceControlStore);
|
const sourceControl = mockedStore(useSourceControlStore);
|
||||||
posthog.getVariant.mockReturnValue(MORE_ONBOARDING_OPTIONS_EXPERIMENT.control);
|
|
||||||
|
|
||||||
const projectsStore = mockedStore(useProjectsStore);
|
const projectsStore = mockedStore(useProjectsStore);
|
||||||
|
|
||||||
|
@ -111,44 +108,6 @@ describe('WorkflowsView', () => {
|
||||||
|
|
||||||
expect(router.currentRoute.value.name).toBe(VIEWS.NEW_WORKFLOW);
|
expect(router.currentRoute.value.name).toBe(VIEWS.NEW_WORKFLOW);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should show courses and templates link for sales users', () => {
|
|
||||||
it('for cloudUser', () => {
|
|
||||||
const pinia = createTestingPinia({ initialState });
|
|
||||||
const userStore = mockedStore(useUsersStore);
|
|
||||||
userStore.currentUserCloudInfo = { role: 'Sales' } as Cloud.UserAccount;
|
|
||||||
const projectsStore = mockedStore(useProjectsStore);
|
|
||||||
projectsStore.currentProject = { scopes: ['workflow:create'] } as Project;
|
|
||||||
const { getAllByTestId } = renderComponent({ pinia });
|
|
||||||
|
|
||||||
expect(getAllByTestId('browse-sales-templates-card').length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('for personalizationAnswers', () => {
|
|
||||||
const pinia = createTestingPinia({ initialState });
|
|
||||||
const userStore = mockedStore(useUsersStore);
|
|
||||||
userStore.currentUser = { personalizationAnswers: { role: 'Sales' } } as IUser;
|
|
||||||
const projectsStore = mockedStore(useProjectsStore);
|
|
||||||
projectsStore.currentProject = { scopes: ['workflow:create'] } as Project;
|
|
||||||
const { getAllByTestId } = renderComponent({ pinia });
|
|
||||||
|
|
||||||
expect(getAllByTestId('browse-sales-templates-card').length).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show courses and templates link for onboardingExperiment', () => {
|
|
||||||
const pinia = createTestingPinia({ initialState });
|
|
||||||
|
|
||||||
const projectsStore = mockedStore(useProjectsStore);
|
|
||||||
projectsStore.currentProject = { scopes: ['workflow:create'] } as Project;
|
|
||||||
|
|
||||||
const posthog = mockedStore(usePostHog);
|
|
||||||
posthog.getVariant.mockReturnValue(MORE_ONBOARDING_OPTIONS_EXPERIMENT.variant);
|
|
||||||
|
|
||||||
const { getAllByTestId } = renderComponent({ pinia });
|
|
||||||
|
|
||||||
expect(getAllByTestId('browse-sales-templates-card').length).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filters', () => {
|
describe('filters', () => {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import ResourcesListLayout, {
|
||||||
} from '@/components/layouts/ResourcesListLayout.vue';
|
} from '@/components/layouts/ResourcesListLayout.vue';
|
||||||
import WorkflowCard from '@/components/WorkflowCard.vue';
|
import WorkflowCard from '@/components/WorkflowCard.vue';
|
||||||
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
import WorkflowTagsDropdown from '@/components/WorkflowTagsDropdown.vue';
|
||||||
import { EnterpriseEditionFeature, MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants';
|
import { EASY_AI_WORKFLOW_EXPERIMENT, EnterpriseEditionFeature, VIEWS } from '@/constants';
|
||||||
import type { IUser, IWorkflowDb } from '@/Interface';
|
import type { IUser, IWorkflowDb } from '@/Interface';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
|
@ -15,7 +15,6 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||||
import { useTagsStore } from '@/stores/tags.store';
|
import { useTagsStore } from '@/stores/tags.store';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import { useTemplatesStore } from '@/stores/templates.store';
|
|
||||||
import { getResourcePermissions } from '@/permissions';
|
import { getResourcePermissions } from '@/permissions';
|
||||||
import { usePostHog } from '@/stores/posthog.store';
|
import { usePostHog } from '@/stores/posthog.store';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
|
@ -33,6 +32,7 @@ import {
|
||||||
} from 'n8n-design-system';
|
} from 'n8n-design-system';
|
||||||
import { pickBy } from 'lodash-es';
|
import { pickBy } from 'lodash-es';
|
||||||
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
||||||
|
import { EASY_AI_WORKFLOW_JSON } from '@/constants.workflows';
|
||||||
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
@ -44,7 +44,6 @@ const workflowsStore = useWorkflowsStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const posthogStore = usePostHog();
|
const posthogStore = usePostHog();
|
||||||
const projectsStore = useProjectsStore();
|
const projectsStore = useProjectsStore();
|
||||||
const templatesStore = useTemplatesStore();
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const uiStore = useUIStore();
|
const uiStore = useUIStore();
|
||||||
const tagsStore = useTagsStore();
|
const tagsStore = useTagsStore();
|
||||||
|
@ -68,6 +67,7 @@ const filters = ref<Filters>({
|
||||||
status: StatusFilter.ALL,
|
status: StatusFilter.ALL,
|
||||||
tags: [],
|
tags: [],
|
||||||
});
|
});
|
||||||
|
const easyAICalloutVisible = ref(true);
|
||||||
|
|
||||||
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||||
const currentUser = computed(() => usersStore.currentUser ?? ({} as IUser));
|
const currentUser = computed(() => usersStore.currentUser ?? ({} as IUser));
|
||||||
|
@ -91,27 +91,12 @@ const statusFilterOptions = computed(() => [
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const userRole = computed(() => {
|
const showEasyAIWorkflowCallout = computed(() => {
|
||||||
const role = usersStore.currentUserCloudInfo?.role;
|
const isEasyAIWorkflowExperimentEnabled =
|
||||||
if (role) return role;
|
posthogStore.getVariant(EASY_AI_WORKFLOW_EXPERIMENT.name) ===
|
||||||
|
EASY_AI_WORKFLOW_EXPERIMENT.variant;
|
||||||
const answers = usersStore.currentUser?.personalizationAnswers;
|
const easyAIWorkflowOnboardingDone = usersStore.isEasyAIWorkflowOnboardingDone;
|
||||||
if (answers && 'role' in answers) {
|
return isEasyAIWorkflowExperimentEnabled && !easyAIWorkflowOnboardingDone;
|
||||||
return answers.role;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isOnboardingExperimentEnabled = computed(() => {
|
|
||||||
return (
|
|
||||||
posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) ===
|
|
||||||
MORE_ONBOARDING_OPTIONS_EXPERIMENT.variant
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const isSalesUser = computed(() => {
|
|
||||||
return ['Sales', 'sales-and-marketing'].includes(userRole.value || '');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const projectPermissions = computed(() => {
|
const projectPermissions = computed(() => {
|
||||||
|
@ -169,22 +154,10 @@ const addWorkflow = () => {
|
||||||
trackEmptyCardClick('blank');
|
trackEmptyCardClick('blank');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getTemplateRepositoryURL = () => templatesStore.websiteTemplateRepositoryURL;
|
|
||||||
|
|
||||||
const trackEmptyCardClick = (option: 'blank' | 'templates' | 'courses') => {
|
const trackEmptyCardClick = (option: 'blank' | 'templates' | 'courses') => {
|
||||||
telemetry.track('User clicked empty page option', {
|
telemetry.track('User clicked empty page option', {
|
||||||
option,
|
option,
|
||||||
});
|
});
|
||||||
if (option === 'templates' && isSalesUser.value) {
|
|
||||||
trackCategoryLinkClick('Sales');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const trackCategoryLinkClick = (category: string) => {
|
|
||||||
telemetry.track(`User clicked Browse ${category} Templates`, {
|
|
||||||
role: usersStore.currentUserCloudInfo?.role,
|
|
||||||
active_workflow_count: workflowsStore.activeWorkflows.length,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
|
@ -286,6 +259,25 @@ onMounted(async () => {
|
||||||
await setFiltersFromQueryString();
|
await setFiltersFromQueryString();
|
||||||
void usersStore.showPersonalizationSurvey();
|
void usersStore.showPersonalizationSurvey();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const openAIWorkflow = async (source: string) => {
|
||||||
|
dismissEasyAICallout();
|
||||||
|
telemetry.track(
|
||||||
|
'User clicked test AI workflow',
|
||||||
|
{
|
||||||
|
source,
|
||||||
|
},
|
||||||
|
{ withPostHog: true },
|
||||||
|
);
|
||||||
|
await router.push({
|
||||||
|
name: VIEWS.WORKFLOW_ONBOARDING,
|
||||||
|
params: { id: EASY_AI_WORKFLOW_JSON.meta.templateId },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const dismissEasyAICallout = () => {
|
||||||
|
easyAICalloutVisible.value = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -305,6 +297,35 @@ onMounted(async () => {
|
||||||
<template #header>
|
<template #header>
|
||||||
<ProjectHeader />
|
<ProjectHeader />
|
||||||
</template>
|
</template>
|
||||||
|
<template #callout>
|
||||||
|
<N8nCallout
|
||||||
|
v-if="showEasyAIWorkflowCallout && easyAICalloutVisible"
|
||||||
|
theme="secondary"
|
||||||
|
icon="robot"
|
||||||
|
:class="$style['easy-ai-workflow-callout']"
|
||||||
|
>
|
||||||
|
{{ i18n.baseText('workflows.list.easyAI') }}
|
||||||
|
<template #trailingContent>
|
||||||
|
<div :class="$style['callout-trailing-content']">
|
||||||
|
<n8n-button
|
||||||
|
data-test-id="easy-ai-button"
|
||||||
|
size="small"
|
||||||
|
type="secondary"
|
||||||
|
@click="openAIWorkflow('callout')"
|
||||||
|
>
|
||||||
|
{{ i18n.baseText('generic.tryNow') }}
|
||||||
|
</n8n-button>
|
||||||
|
<N8nIcon
|
||||||
|
size="small"
|
||||||
|
icon="times"
|
||||||
|
:title="i18n.baseText('generic.dismiss')"
|
||||||
|
class="clickable"
|
||||||
|
@click="dismissEasyAICallout"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</N8nCallout>
|
||||||
|
</template>
|
||||||
<template #default="{ data, updateItemSize }">
|
<template #default="{ data, updateItemSize }">
|
||||||
<WorkflowCard
|
<WorkflowCard
|
||||||
data-test-id="resources-list-item"
|
data-test-id="resources-list-item"
|
||||||
|
@ -326,7 +347,7 @@ onMounted(async () => {
|
||||||
: i18n.baseText('workflows.empty.heading.userNotSetup')
|
: i18n.baseText('workflows.empty.heading.userNotSetup')
|
||||||
}}
|
}}
|
||||||
</N8nHeading>
|
</N8nHeading>
|
||||||
<N8nText v-if="!isOnboardingExperimentEnabled" size="large" color="text-base">
|
<N8nText size="large" color="text-base">
|
||||||
{{ emptyListDescription }}
|
{{ emptyListDescription }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
</div>
|
</div>
|
||||||
|
@ -345,40 +366,18 @@ onMounted(async () => {
|
||||||
{{ i18n.baseText('workflows.empty.startFromScratch') }}
|
{{ i18n.baseText('workflows.empty.startFromScratch') }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
</N8nCard>
|
</N8nCard>
|
||||||
<a
|
<N8nCard
|
||||||
v-if="isSalesUser || isOnboardingExperimentEnabled"
|
v-if="showEasyAIWorkflowCallout"
|
||||||
href="https://docs.n8n.io/courses/#available-courses"
|
|
||||||
:class="$style.emptyStateCard"
|
:class="$style.emptyStateCard"
|
||||||
target="_blank"
|
hoverable
|
||||||
|
data-test-id="easy-ai-workflow-card"
|
||||||
|
@click="openAIWorkflow('empty')"
|
||||||
>
|
>
|
||||||
<N8nCard
|
<N8nIcon :class="$style.emptyStateCardIcon" icon="robot" />
|
||||||
hoverable
|
<N8nText size="large" class="mt-xs pl-2xs pr-2xs" color="text-dark">
|
||||||
data-test-id="browse-sales-templates-card"
|
{{ i18n.baseText('workflows.empty.easyAI') }}
|
||||||
@click="trackEmptyCardClick('courses')"
|
</N8nText>
|
||||||
>
|
</N8nCard>
|
||||||
<N8nIcon :class="$style.emptyStateCardIcon" icon="graduation-cap" />
|
|
||||||
<N8nText size="large" class="mt-xs" color="text-dark">
|
|
||||||
{{ i18n.baseText('workflows.empty.learnN8n') }}
|
|
||||||
</N8nText>
|
|
||||||
</N8nCard>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
v-if="isSalesUser || isOnboardingExperimentEnabled"
|
|
||||||
:href="getTemplateRepositoryURL()"
|
|
||||||
:class="$style.emptyStateCard"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<N8nCard
|
|
||||||
hoverable
|
|
||||||
data-test-id="browse-sales-templates-card"
|
|
||||||
@click="trackEmptyCardClick('templates')"
|
|
||||||
>
|
|
||||||
<N8nIcon :class="$style.emptyStateCardIcon" icon="box-open" />
|
|
||||||
<N8nText size="large" class="mt-xs" color="text-dark">
|
|
||||||
{{ i18n.baseText('workflows.empty.browseTemplates') }}
|
|
||||||
</N8nText>
|
|
||||||
</N8nCard>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #filters="{ setKeyValue }">
|
<template #filters="{ setKeyValue }">
|
||||||
|
@ -430,6 +429,19 @@ onMounted(async () => {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.easy-ai-workflow-callout {
|
||||||
|
// Make the callout padding in line with workflow cards
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
padding-left: var(--spacing-s);
|
||||||
|
padding-right: var(--spacing-m);
|
||||||
|
|
||||||
|
.callout-trailing-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.emptyStateCard {
|
.emptyStateCard {
|
||||||
width: 192px;
|
width: 192px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -2779,6 +2779,7 @@ export interface IUserSettings {
|
||||||
userActivatedAt?: number;
|
userActivatedAt?: number;
|
||||||
allowSSOManualLogin?: boolean;
|
allowSSOManualLogin?: boolean;
|
||||||
npsSurvey?: NpsSurveyState;
|
npsSurvey?: NpsSurveyState;
|
||||||
|
easyAIWorkflowOnboarded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProcessedDataConfig {
|
export interface IProcessedDataConfig {
|
||||||
|
|
Loading…
Reference in a new issue