mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -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();
|
||||
|
||||
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({
|
||||
userActivated: z.boolean().optional(),
|
||||
allowSSOManualLogin: z.boolean().optional(),
|
||||
easyAIWorkflowOnboarded: z.boolean().optional(),
|
||||
}) {}
|
||||
|
|
|
@ -173,4 +173,5 @@ export interface FrontendSettings {
|
|||
};
|
||||
betaFeatures: FrontendBetaFeatures[];
|
||||
virtualSchemaView: boolean;
|
||||
easyAIWorkflowOnboarded: boolean;
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ type ExceptionPaths = {
|
|||
processedDataManager: IProcessedDataConfig;
|
||||
'userManagement.isInstanceOwnerSetUp': boolean;
|
||||
'ui.banners.dismissed': string[] | undefined;
|
||||
easyAIWorkflowOnboarded: boolean | undefined;
|
||||
};
|
||||
|
||||
// -----------------------------------
|
||||
|
|
|
@ -232,6 +232,7 @@ export class FrontendService {
|
|||
},
|
||||
betaFeatures: this.frontendConfig.betaFeatures,
|
||||
virtualSchemaView: config.getEnv('virtualSchemaView'),
|
||||
easyAIWorkflowOnboarded: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -274,6 +275,11 @@ export class FrontendService {
|
|||
}
|
||||
|
||||
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 isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
|
||||
|
|
|
@ -249,6 +249,16 @@ export interface IWorkflowDataCreate extends IWorkflowDataUpdate {
|
|||
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 {
|
||||
meta: WorkflowMetadata;
|
||||
}
|
||||
|
|
|
@ -127,4 +127,5 @@ export const defaultSettings: FrontendSettings = {
|
|||
},
|
||||
betaFeatures: [],
|
||||
virtualSchemaView: false,
|
||||
easyAIWorkflowOnboarded: false,
|
||||
};
|
||||
|
|
|
@ -79,7 +79,6 @@ import {
|
|||
REPORTED_SOURCE_OTHER,
|
||||
REPORTED_SOURCE_OTHER_KEY,
|
||||
VIEWS,
|
||||
MORE_ONBOARDING_OPTIONS_EXPERIMENT,
|
||||
COMMUNITY_PLUS_ENROLLMENT_MODAL,
|
||||
} from '@/constants';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
|
@ -552,12 +551,9 @@ const onSave = () => {
|
|||
};
|
||||
|
||||
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
|
||||
// we try again after closing the modal
|
||||
if (route.name !== VIEWS.HOMEPAGE && !isPartOfOnboardingExperiment) {
|
||||
if (route.name !== VIEWS.HOMEPAGE) {
|
||||
void router.replace({ name: VIEWS.HOMEPAGE });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -20,7 +20,8 @@ import { useWorkflowResourceLocatorModes } from './useWorkflowResourceLocatorMod
|
|||
import { useWorkflowResourcesLocator } from './useWorkflowResourcesLocator';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
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 {
|
||||
modelValue: INodeParameterResourceLocator;
|
||||
|
@ -231,7 +232,7 @@ const onAddResourceClicked = () => {
|
|||
};
|
||||
|
||||
window.open(
|
||||
`/workflows/onboarding/${SAMPLE_SUBWORKFLOW_WORKFLOW_ID}?${urlSearchParams.toString()}`,
|
||||
`/workflows/onboarding/${SAMPLE_SUBWORKFLOW_WORKFLOW.meta.templateId}?${urlSearchParams.toString()}`,
|
||||
'_blank',
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { computed, useSlots } from 'vue';
|
||||
import type { BannerName } from 'n8n-workflow';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
interface Props {
|
||||
name: BannerName;
|
||||
|
@ -10,6 +11,8 @@ interface Props {
|
|||
dismissible?: boolean;
|
||||
}
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const uiStore = useUIStore();
|
||||
const slots = useSlots();
|
||||
|
||||
|
@ -51,7 +54,7 @@ async function onCloseClick() {
|
|||
v-if="dismissible"
|
||||
size="small"
|
||||
icon="times"
|
||||
title="Dismiss"
|
||||
:title="i18n.baseText('generic.dismiss')"
|
||||
class="clickable"
|
||||
:data-test-id="`banner-${props.name}-close`"
|
||||
@click="onCloseClick"
|
||||
|
|
|
@ -36,6 +36,7 @@ import type { PushMessageQueueItem } from '@/types';
|
|||
import { useAssistantStore } from '@/stores/assistant.store';
|
||||
import NodeExecutionErrorMessage from '@/components/NodeExecutionErrorMessage.vue';
|
||||
import type { IExecutionResponse } from '@/Interface';
|
||||
import { EASY_AI_WORKFLOW_JSON } from '@/constants.workflows';
|
||||
|
||||
export function usePushConnection({ router }: { router: ReturnType<typeof useRouter> }) {
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
|
@ -199,6 +200,23 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
|
|||
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 { activeExecutionId } = workflowsStore;
|
||||
if (executionId !== activeExecutionId) {
|
||||
|
|
|
@ -1181,6 +1181,14 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
|||
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 {
|
||||
setDocumentTitle,
|
||||
resolveParameter,
|
||||
|
@ -1207,5 +1215,6 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
|||
promptSaveUnsavedWorkflowChanges,
|
||||
initState,
|
||||
getNodeParametersWithResolvedExpressions,
|
||||
containsNodeFromPackage,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import type {
|
||||
EnterpriseEditionFeatureKey,
|
||||
EnterpriseEditionFeatureValue,
|
||||
INodeUi,
|
||||
IWorkflowDataCreate,
|
||||
NodeCreatorOpenSource,
|
||||
} from './Interface';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
|
@ -696,23 +694,24 @@ export const AI_ASSISTANT_EXPERIMENT = {
|
|||
variant: 'variant',
|
||||
};
|
||||
|
||||
export const MORE_ONBOARDING_OPTIONS_EXPERIMENT = {
|
||||
name: '022_more_onboarding_options',
|
||||
control: 'control',
|
||||
variant: 'variant',
|
||||
};
|
||||
|
||||
export const CREDENTIAL_DOCS_EXPERIMENT = {
|
||||
name: '024_credential_docs',
|
||||
control: 'control',
|
||||
variant: 'variant',
|
||||
};
|
||||
|
||||
export const EASY_AI_WORKFLOW_EXPERIMENT = {
|
||||
name: '026_easy_ai_workflow',
|
||||
control: 'control',
|
||||
variant: 'variant',
|
||||
};
|
||||
|
||||
export const EXPERIMENTS_TO_TRACK = [
|
||||
TEMPLATE_CREDENTIAL_SETUP_EXPERIMENT,
|
||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT.name,
|
||||
AI_ASSISTANT_EXPERIMENT.name,
|
||||
MORE_ONBOARDING_OPTIONS_EXPERIMENT.name,
|
||||
CREDENTIAL_DOCS_EXPERIMENT.name,
|
||||
EASY_AI_WORKFLOW_EXPERIMENT.name,
|
||||
];
|
||||
|
||||
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 SAMPLE_SUBWORKFLOW_WORKFLOW_ID = '0';
|
||||
|
||||
export const NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL = 'new-sample-sub-workflow-created';
|
||||
|
||||
export const SAMPLE_SUBWORKFLOW_WORKFLOW: IWorkflowDataCreate = {
|
||||
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: {},
|
||||
};
|
||||
export const AI_NODES_PACKAGE_NAME = '@n8n/n8n-nodes-langchain';
|
||||
|
|
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.settings": "Settings",
|
||||
"generic.service": "the service",
|
||||
"generic.tryNow": "Try now",
|
||||
"generic.dismiss": "Dismiss",
|
||||
"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.confirmButtonText": "Save",
|
||||
|
@ -2294,6 +2296,8 @@
|
|||
"workflows.empty.browseTemplates": "Explore workflow templates",
|
||||
"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.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.static": "Shared with {projectName}",
|
||||
"workflows.shareModal.select.placeholder": "Add users...",
|
||||
|
|
|
@ -74,6 +74,16 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
|
|||
|
||||
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 user = currentUser.value;
|
||||
if (!user) {
|
||||
|
@ -410,5 +420,7 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
|
|||
sendConfirmationEmail,
|
||||
updateGlobalRole,
|
||||
reset,
|
||||
isEasyAIWorkflowOnboardingDone,
|
||||
setEasyAIWorkflowOnboardingDone,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AI_NODES_PACKAGE_NAME,
|
||||
CHAT_TRIGGER_NODE_TYPE,
|
||||
DEFAULT_NEW_WORKFLOW_NAME,
|
||||
DUPLICATE_POSTFFIX,
|
||||
|
@ -86,6 +87,8 @@ import { useRouter } from 'vue-router';
|
|||
import { useSettingsStore } from './settings.store';
|
||||
import { closeFormPopupWindow, openFormPopupWindow } from '@/utils/executionUtils';
|
||||
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']> } = {
|
||||
name: '',
|
||||
|
@ -119,6 +122,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||
const settingsStore = useSettingsStore();
|
||||
const rootStore = useRootStore();
|
||||
const nodeHelpers = useNodeHelpers();
|
||||
const usersStore = useUsersStore();
|
||||
|
||||
// -1 means the backend chooses the default
|
||||
// 0 is the old flow
|
||||
|
@ -1415,12 +1419,25 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||
(sendData as unknown as IDataObject).projectId = projectStore.currentProjectId;
|
||||
}
|
||||
|
||||
return await makeRestApiRequest(
|
||||
const newWorkflow = await makeRestApiRequest<IWorkflowDb>(
|
||||
rootStore.restApiContext,
|
||||
'POST',
|
||||
'/workflows',
|
||||
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(
|
||||
|
@ -1432,12 +1449,24 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
|||
data.settings = undefined;
|
||||
}
|
||||
|
||||
return await makeRestApiRequest(
|
||||
const updatedWorkflow = await makeRestApiRequest<IWorkflowDb>(
|
||||
rootStore.restApiContext,
|
||||
'PATCH',
|
||||
`/workflows/${id}${forceSave ? '?forceSave=true' : ''}`,
|
||||
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> {
|
||||
|
|
|
@ -5,17 +5,15 @@ import { useRouter } from 'vue-router';
|
|||
import { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
import { usePostHog } from '@/stores/posthog.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
|
||||
import type { IFormBoxConfig } from '@/Interface';
|
||||
import { MORE_ONBOARDING_OPTIONS_EXPERIMENT, VIEWS } from '@/constants';
|
||||
import { VIEWS } from '@/constants';
|
||||
|
||||
import AuthView from '@/views/AuthView.vue';
|
||||
|
||||
const posthogStore = usePostHog();
|
||||
const settingsStore = useSettingsStore();
|
||||
const uiStore = useUIStore();
|
||||
const usersStore = useUsersStore();
|
||||
|
@ -85,9 +83,6 @@ const formConfig: IFormBoxConfig = reactive({
|
|||
const onSubmit = async (values: { [key: string]: string | boolean }) => {
|
||||
try {
|
||||
const forceRedirectedHere = settingsStore.showSetupPage;
|
||||
const isPartOfOnboardingExperiment =
|
||||
posthogStore.getVariant(MORE_ONBOARDING_OPTIONS_EXPERIMENT.name) ===
|
||||
MORE_ONBOARDING_OPTIONS_EXPERIMENT.variant;
|
||||
loading.value = true;
|
||||
await usersStore.createOwner(
|
||||
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);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (forceRedirectedHere) {
|
||||
if (isPartOfOnboardingExperiment) {
|
||||
await router.push({ name: VIEWS.WORKFLOWS });
|
||||
} else {
|
||||
await router.push({ name: VIEWS.HOMEPAGE });
|
||||
}
|
||||
await router.push({ name: VIEWS.HOMEPAGE });
|
||||
} else {
|
||||
await router.push({ name: VIEWS.USERS_SETTINGS });
|
||||
}
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
<script setup lang="ts">
|
||||
import { useLoadingService } from '@/composables/useLoadingService';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import {
|
||||
NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL,
|
||||
SAMPLE_SUBWORKFLOW_WORKFLOW,
|
||||
SAMPLE_SUBWORKFLOW_WORKFLOW_ID,
|
||||
VIEWS,
|
||||
} from '@/constants';
|
||||
import { NEW_SAMPLE_WORKFLOW_CREATED_CHANNEL, VIEWS } from '@/constants';
|
||||
import { useTemplatesStore } from '@/stores/templates.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import type { IWorkflowDataCreate } from '@/Interface';
|
||||
import { EASY_AI_WORKFLOW_JSON, SAMPLE_SUBWORKFLOW_WORKFLOW } from '@/constants.workflows';
|
||||
|
||||
const loadingService = useLoadingService();
|
||||
const templateStore = useTemplatesStore();
|
||||
|
@ -21,10 +17,14 @@ const route = useRoute();
|
|||
const i18n = useI18n();
|
||||
|
||||
const openWorkflowTemplate = async (templateId: string) => {
|
||||
if (templateId === SAMPLE_SUBWORKFLOW_WORKFLOW_ID) {
|
||||
if (templateId === SAMPLE_SUBWORKFLOW_WORKFLOW.meta.templateId) {
|
||||
await openSampleSubworkflow();
|
||||
return;
|
||||
}
|
||||
if (templateId === EASY_AI_WORKFLOW_JSON.meta.templateId) {
|
||||
await openEasyAIWorkflow();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
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 () => {
|
||||
try {
|
||||
loadingService.startLoading();
|
||||
|
|
|
@ -5,10 +5,9 @@ import { useUsersStore } from '@/stores/users.store';
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
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 { usePostHog } from '@/stores/posthog.store';
|
||||
import type { Cloud, IUser, IWorkflowDb } from '@/Interface';
|
||||
import type { IUser, IWorkflowDb } from '@/Interface';
|
||||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import type { Project } from '@/types/projects.types';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
|
@ -73,9 +72,7 @@ describe('WorkflowsView', () => {
|
|||
|
||||
describe('when onboardingExperiment -> False', () => {
|
||||
const pinia = createTestingPinia({ initialState });
|
||||
const posthog = mockedStore(usePostHog);
|
||||
const sourceControl = mockedStore(useSourceControlStore);
|
||||
posthog.getVariant.mockReturnValue(MORE_ONBOARDING_OPTIONS_EXPERIMENT.control);
|
||||
|
||||
const projectsStore = mockedStore(useProjectsStore);
|
||||
|
||||
|
@ -111,44 +108,6 @@ describe('WorkflowsView', () => {
|
|||
|
||||
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', () => {
|
||||
|
|
|
@ -6,7 +6,7 @@ import ResourcesListLayout, {
|
|||
} from '@/components/layouts/ResourcesListLayout.vue';
|
||||
import WorkflowCard from '@/components/WorkflowCard.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 { useUIStore } from '@/stores/ui.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
|
@ -15,7 +15,6 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
|||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useTagsStore } from '@/stores/tags.store';
|
||||
import { useProjectsStore } from '@/stores/projects.store';
|
||||
import { useTemplatesStore } from '@/stores/templates.store';
|
||||
import { getResourcePermissions } from '@/permissions';
|
||||
import { usePostHog } from '@/stores/posthog.store';
|
||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||
|
@ -33,6 +32,7 @@ import {
|
|||
} from 'n8n-design-system';
|
||||
import { pickBy } from 'lodash-es';
|
||||
import ProjectHeader from '@/components/Projects/ProjectHeader.vue';
|
||||
import { EASY_AI_WORKFLOW_JSON } from '@/constants.workflows';
|
||||
|
||||
const i18n = useI18n();
|
||||
const route = useRoute();
|
||||
|
@ -44,7 +44,6 @@ const workflowsStore = useWorkflowsStore();
|
|||
const settingsStore = useSettingsStore();
|
||||
const posthogStore = usePostHog();
|
||||
const projectsStore = useProjectsStore();
|
||||
const templatesStore = useTemplatesStore();
|
||||
const telemetry = useTelemetry();
|
||||
const uiStore = useUIStore();
|
||||
const tagsStore = useTagsStore();
|
||||
|
@ -68,6 +67,7 @@ const filters = ref<Filters>({
|
|||
status: StatusFilter.ALL,
|
||||
tags: [],
|
||||
});
|
||||
const easyAICalloutVisible = ref(true);
|
||||
|
||||
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||
const currentUser = computed(() => usersStore.currentUser ?? ({} as IUser));
|
||||
|
@ -91,27 +91,12 @@ const statusFilterOptions = computed(() => [
|
|||
},
|
||||
]);
|
||||
|
||||
const userRole = computed(() => {
|
||||
const role = usersStore.currentUserCloudInfo?.role;
|
||||
if (role) return role;
|
||||
|
||||
const answers = usersStore.currentUser?.personalizationAnswers;
|
||||
if (answers && 'role' in answers) {
|
||||
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 showEasyAIWorkflowCallout = computed(() => {
|
||||
const isEasyAIWorkflowExperimentEnabled =
|
||||
posthogStore.getVariant(EASY_AI_WORKFLOW_EXPERIMENT.name) ===
|
||||
EASY_AI_WORKFLOW_EXPERIMENT.variant;
|
||||
const easyAIWorkflowOnboardingDone = usersStore.isEasyAIWorkflowOnboardingDone;
|
||||
return isEasyAIWorkflowExperimentEnabled && !easyAIWorkflowOnboardingDone;
|
||||
});
|
||||
|
||||
const projectPermissions = computed(() => {
|
||||
|
@ -169,22 +154,10 @@ const addWorkflow = () => {
|
|||
trackEmptyCardClick('blank');
|
||||
};
|
||||
|
||||
const getTemplateRepositoryURL = () => templatesStore.websiteTemplateRepositoryURL;
|
||||
|
||||
const trackEmptyCardClick = (option: 'blank' | 'templates' | 'courses') => {
|
||||
telemetry.track('User clicked empty page 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 () => {
|
||||
|
@ -286,6 +259,25 @@ onMounted(async () => {
|
|||
await setFiltersFromQueryString();
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -305,6 +297,35 @@ onMounted(async () => {
|
|||
<template #header>
|
||||
<ProjectHeader />
|
||||
</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 }">
|
||||
<WorkflowCard
|
||||
data-test-id="resources-list-item"
|
||||
|
@ -326,7 +347,7 @@ onMounted(async () => {
|
|||
: i18n.baseText('workflows.empty.heading.userNotSetup')
|
||||
}}
|
||||
</N8nHeading>
|
||||
<N8nText v-if="!isOnboardingExperimentEnabled" size="large" color="text-base">
|
||||
<N8nText size="large" color="text-base">
|
||||
{{ emptyListDescription }}
|
||||
</N8nText>
|
||||
</div>
|
||||
|
@ -345,40 +366,18 @@ onMounted(async () => {
|
|||
{{ i18n.baseText('workflows.empty.startFromScratch') }}
|
||||
</N8nText>
|
||||
</N8nCard>
|
||||
<a
|
||||
v-if="isSalesUser || isOnboardingExperimentEnabled"
|
||||
href="https://docs.n8n.io/courses/#available-courses"
|
||||
<N8nCard
|
||||
v-if="showEasyAIWorkflowCallout"
|
||||
:class="$style.emptyStateCard"
|
||||
target="_blank"
|
||||
hoverable
|
||||
data-test-id="easy-ai-workflow-card"
|
||||
@click="openAIWorkflow('empty')"
|
||||
>
|
||||
<N8nCard
|
||||
hoverable
|
||||
data-test-id="browse-sales-templates-card"
|
||||
@click="trackEmptyCardClick('courses')"
|
||||
>
|
||||
<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>
|
||||
<N8nIcon :class="$style.emptyStateCardIcon" icon="robot" />
|
||||
<N8nText size="large" class="mt-xs pl-2xs pr-2xs" color="text-dark">
|
||||
{{ i18n.baseText('workflows.empty.easyAI') }}
|
||||
</N8nText>
|
||||
</N8nCard>
|
||||
</div>
|
||||
</template>
|
||||
<template #filters="{ setKeyValue }">
|
||||
|
@ -430,6 +429,19 @@ onMounted(async () => {
|
|||
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 {
|
||||
width: 192px;
|
||||
text-align: center;
|
||||
|
|
|
@ -2779,6 +2779,7 @@ export interface IUserSettings {
|
|||
userActivatedAt?: number;
|
||||
allowSSOManualLogin?: boolean;
|
||||
npsSurvey?: NpsSurveyState;
|
||||
easyAIWorkflowOnboarded?: boolean;
|
||||
}
|
||||
|
||||
export interface IProcessedDataConfig {
|
||||
|
|
Loading…
Reference in a new issue