diff --git a/packages/frontend/@n8n/stores/src/users.store.test.ts b/packages/frontend/@n8n/stores/src/users.store.test.ts index af7c458fd0c..32746b62484 100644 --- a/packages/frontend/@n8n/stores/src/users.store.test.ts +++ b/packages/frontend/@n8n/stores/src/users.store.test.ts @@ -307,51 +307,6 @@ describe('users.store', () => { }); }); - describe('personalizedNodeTypes', () => { - const setCurrentUserWithAnswers = ( - usersStore: ReturnType, - personalizationAnswers?: object, - ) => { - usersStore.usersById['1'] = { - ...mockUser, - isDefaultUser: false, - isPendingUser: false, - mfaEnabled: false, - ...(personalizationAnswers ? { personalizationAnswers } : {}), - } as never; - usersStore.currentUserId = '1'; - }; - - it('returns an empty list when no resolver is registered', () => { - const usersStore = useUsersStore(); - setCurrentUserWithAnswers(usersStore, { version: 'v4' }); - - expect(usersStore.personalizedNodeTypes).toEqual([]); - }); - - it('delegates to the injected resolver when one is registered', () => { - const usersStore = useUsersStore(); - setCurrentUserWithAnswers(usersStore, { version: 'v4' }); - - const resolver = vi.fn(() => ['n8n-nodes-base.webhook']); - usersStore.setNodeTypesResolver(resolver); - - expect(usersStore.personalizedNodeTypes).toEqual(['n8n-nodes-base.webhook']); - expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ version: 'v4' })); - }); - - it('returns an empty list when the current user has no answers', () => { - const usersStore = useUsersStore(); - setCurrentUserWithAnswers(usersStore); - - const resolver = vi.fn(() => ['n8n-nodes-base.webhook']); - usersStore.setNodeTypesResolver(resolver); - - expect(usersStore.personalizedNodeTypes).toEqual([]); - expect(resolver).not.toHaveBeenCalled(); - }); - }); - describe('fetchUsers', () => { it('does not fetch when the injected permission check denies listing (default)', async () => { const usersStore = useUsersStore(); diff --git a/packages/frontend/@n8n/stores/src/users.store.ts b/packages/frontend/@n8n/stores/src/users.store.ts index c6e75cbc163..df17f191a8a 100644 --- a/packages/frontend/@n8n/stores/src/users.store.ts +++ b/packages/frontend/@n8n/stores/src/users.store.ts @@ -18,7 +18,6 @@ import type { IUser, CurrentUserResponse, IPersonalizationLatestVersion, - IPersonalizationSurveyVersions, } from '@n8n/rest-api-client/api/users'; import * as usersApi from '@n8n/rest-api-client/api/users'; import { useAsyncState } from '@vueuse/core'; @@ -39,13 +38,6 @@ import { useRootStore } from './useRootStore'; */ const PERSONALIZATION_MODAL_KEY = 'personalization'; -/** - * Resolves a user's personalization-survey answers to recommended node types. - * Injected by the app (see `app/init.ts`) so the store avoids importing the - * node-type constants that the real implementation depends on. - */ -type PersonalizedNodeTypesResolver = (answers: IPersonalizationSurveyVersions) => string[]; - const _isPendingUser = (user: IUserResponse | null) => !!user?.isPending; const _isInstanceOwner = (user: IUserResponse | null) => user?.role === ROLE.Owner; const _isDefaultUser = (user: IUserResponse | null) => @@ -94,12 +86,6 @@ export const useUsersStore = defineStore(STORES.USERS, () => { canListUsers.value = resolvers.listUsers; }; - // Maps a user's personalization-survey answers to recommended node types. - const nodeTypesResolver = ref(() => []); - const setNodeTypesResolver = (resolver: PersonalizedNodeTypesResolver) => { - nodeTypesResolver.value = resolver; - }; - // Stores const rootStore = useRootStore(); @@ -154,19 +140,6 @@ export const useUsersStore = defineStore(STORES.USERS, () => { } }; - const personalizedNodeTypes = computed(() => { - const user = currentUser.value; - if (!user) { - return []; - } - - const answers = user.personalizationAnswers; - if (!answers) { - return []; - } - return nodeTypesResolver.value(answers); - }); - const usersLimitNotReached = computed( (): boolean => userQuota.value === -1 || userQuota.value > allUsers.value.length, ); @@ -511,7 +484,6 @@ export const useUsersStore = defineStore(STORES.USERS, () => { isAdminOrOwner, mfaEnabled, globalRoleName, - personalizedNodeTypes, userClaimedAiCredits, isEasyAIWorkflowOnboardingDone, canUserUpdateVersion, @@ -526,7 +498,6 @@ export const useUsersStore = defineStore(STORES.USERS, () => { registerLogoutHook, registerModalOpeners, setPermissionsResolvers, - setNodeTypesResolver, createOwner, validateSignupToken, acceptInvitation, diff --git a/packages/frontend/editor-ui/src/app/init.test.ts b/packages/frontend/editor-ui/src/app/init.test.ts index 069297c86ba..755cee10f37 100644 --- a/packages/frontend/editor-ui/src/app/init.test.ts +++ b/packages/frontend/editor-ui/src/app/init.test.ts @@ -36,7 +36,6 @@ vi.mock('@/features/settings/users/users.store', () => ({ registerLogoutHook: vi.fn(), registerModalOpeners: vi.fn(), setPermissionsResolvers: vi.fn(), - setNodeTypesResolver: vi.fn(), setUserQuota: vi.fn(), }), })); diff --git a/packages/frontend/editor-ui/src/app/init.ts b/packages/frontend/editor-ui/src/app/init.ts index bf0f8f854c4..8fc332ced4a 100644 --- a/packages/frontend/editor-ui/src/app/init.ts +++ b/packages/frontend/editor-ui/src/app/init.ts @@ -24,7 +24,6 @@ import { useUIStore } from '@/app/stores/ui.store'; import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store'; import { useSSOStore } from '@/features/settings/sso/sso.store'; import { useUsersStore } from '@/features/settings/users/users.store'; -import { getPersonalizedNodeTypes } from '@/features/settings/users/users.utils'; import { useVersionsStore } from '@/app/stores/versions.store'; import { useBannersStore } from '@/features/shared/banners/banners.store'; import { useI18n } from '@n8n/i18n'; @@ -135,12 +134,11 @@ export async function initializeAuthenticatedFeatures( usersStore.registerModalOpeners(modalOpeners); versionsStore.registerModalOpeners(modalOpeners); - // Provide the app-side capabilities `users.store` no longer imports directly - // after moving into `@n8n/stores` (RBAC check + survey-to-node-types mapping). + // Provide the app-side capability `users.store` no longer imports directly + // after moving into `@n8n/stores` (RBAC check). usersStore.setPermissionsResolvers({ listUsers: () => hasPermission(['rbac'], { rbac: { scope: 'user:list' } }), }); - usersStore.setNodeTypesResolver(getPersonalizedNodeTypes); if (!settingsStore.isPreviewMode) { usersStore.setUserQuota(settingsStore.userManagement.quota); diff --git a/packages/frontend/editor-ui/src/features/settings/users/users.utils.ts b/packages/frontend/editor-ui/src/features/settings/users/users.utils.ts index a502b3f8116..89b86947c78 100644 --- a/packages/frontend/editor-ui/src/features/settings/users/users.utils.ts +++ b/packages/frontend/editor-ui/src/features/settings/users/users.utils.ts @@ -1,333 +1,14 @@ -import { - CALENDLY_TRIGGER_NODE_TYPE, - CLEARBIT_NODE_TYPE, - SCHEDULE_TRIGGER_NODE_TYPE, - ELASTIC_SECURITY_NODE_TYPE, - EMAIL_SEND_NODE_TYPE, - EXECUTE_COMMAND_NODE_TYPE, - GITHUB_TRIGGER_NODE_TYPE, - HTTP_REQUEST_NODE_TYPE, - IF_NODE_TYPE, - ITEM_LISTS_NODE_TYPE, - JIRA_TRIGGER_NODE_TYPE, - MICROSOFT_EXCEL_NODE_TYPE, - MICROSOFT_TEAMS_NODE_TYPE, - PAGERDUTY_NODE_TYPE, - QUICKBOOKS_NODE_TYPE, - SALESFORCE_NODE_TYPE, - SEGMENT_NODE_TYPE, - SET_NODE_TYPE, - SLACK_NODE_TYPE, - SPREADSHEET_FILE_NODE_TYPE, - SWITCH_NODE_TYPE, - WEBHOOK_NODE_TYPE, - XERO_NODE_TYPE, - ZENDESK_TRIGGER_NODE_TYPE, - WOOCOMMERCE_TRIGGER_NODE_TYPE, - HUBSPOT_TRIGGER_NODE_TYPE, - WORKABLE_TRIGGER_NODE_TYPE, - NOTION_TRIGGER_NODE_TYPE, - THE_HIVE_TRIGGER_NODE_TYPE, - ZENDESK_NODE_TYPE, - SERVICENOW_NODE_TYPE, - JIRA_NODE_TYPE, - BAMBOO_HR_NODE_TYPE, - GOOGLE_SHEETS_NODE_TYPE, - CODE_NODE_TYPE, -} from '@/app/constants'; -import { - COMPANY_SIZE_1000_OR_MORE, - COMPANY_SIZE_500_999, - FINANCE_WORK_AREA, - IT_ENGINEERING_WORK_AREA, - PRODUCT_WORK_AREA, - SALES_BUSINESSDEV_WORK_AREA, - SECURITY_WORK_AREA, - COMPANY_SIZE_KEY, - WORK_AREA_KEY, - CODING_SKILL_KEY, - COMPANY_TYPE_KEY, - ECOMMERCE_COMPANY_TYPE, - MSP_COMPANY_TYPE, - PERSONAL_COMPANY_TYPE, - AUTOMATION_GOAL_KEY, - OTHER_AUTOMATION_GOAL, - NOT_SURE_YET_GOAL, - CUSTOMER_INTEGRATIONS_GOAL, - CUSTOMER_SUPPORT_GOAL, - FINANCE_ACCOUNTING_GOAL, - SALES_MARKETING_GOAL, - HR_GOAL, - OPERATIONS_GOAL, - PRODUCT_GOAL, - SECURITY_GOAL, -} from './users.constants'; import { ROLE } from '@n8n/api-types'; -import type { - IPersonalizationSurveyAnswersV1, - IPersonalizationSurveyAnswersV2, - IPersonalizationSurveyAnswersV3, - IPersonalizationSurveyVersions, - IUser, -} from '@n8n/rest-api-client/api/users'; +import type { IUser } from '@n8n/rest-api-client/api/users'; import type { ILogInStatus } from './users.types'; -import type { IPersonalizationSurveyAnswersV4 } from 'n8n-workflow'; /* Utility functions used to handle users in n8n */ -function isPersonalizationSurveyV2OrLater( - data: IPersonalizationSurveyVersions, -): data is - | IPersonalizationSurveyAnswersV2 - | IPersonalizationSurveyAnswersV3 - | IPersonalizationSurveyAnswersV4 { - return 'version' in data; -} - export const LOGIN_STATUS: { LoggedIn: ILogInStatus; LoggedOut: ILogInStatus } = { LoggedIn: 'LoggedIn', // Can be owner or member or default user LoggedOut: 'LoggedOut', // Can only be logged out if UM has been setup }; export const isUserGlobalOwner = (user: IUser): boolean => user.role === ROLE.Owner; - -export function getPersonalizedNodeTypes( - answers: - | IPersonalizationSurveyAnswersV1 - | IPersonalizationSurveyAnswersV2 - | IPersonalizationSurveyAnswersV3 - | IPersonalizationSurveyAnswersV4 - | null, -): string[] { - if (!answers) { - return []; - } - - if (isPersonalizationSurveyV2OrLater(answers)) { - return getPersonalizationSurveyV2OrLater(answers); - } - - return getPersonalizationSurveyV1(answers); -} - -function getPersonalizationSurveyV2OrLater( - answers: - | IPersonalizationSurveyAnswersV2 - | IPersonalizationSurveyAnswersV3 - | IPersonalizationSurveyAnswersV4, -) { - let nodeTypes: string[] = []; - - const { version, ...data } = answers; - if (Object.keys(data).length === 0) { - return []; - } - - const companySize = answers[COMPANY_SIZE_KEY]; - const companyType = answers[COMPANY_TYPE_KEY]; - const automationGoal = AUTOMATION_GOAL_KEY in answers ? answers[AUTOMATION_GOAL_KEY] : undefined; - - let codingSkill = null; - if (CODING_SKILL_KEY in answers && answers[CODING_SKILL_KEY]) { - codingSkill = parseInt(answers[CODING_SKILL_KEY], 10); - codingSkill = isNaN(codingSkill) ? 0 : codingSkill; - } - - // slot 1 trigger - if (companyType === ECOMMERCE_COMPANY_TYPE) { - nodeTypes = nodeTypes.concat(WOOCOMMERCE_TRIGGER_NODE_TYPE); - } else if (companyType === MSP_COMPANY_TYPE) { - nodeTypes = nodeTypes.concat(JIRA_TRIGGER_NODE_TYPE); - } else if ( - (companyType === PERSONAL_COMPANY_TYPE || - automationGoal === OTHER_AUTOMATION_GOAL || - automationGoal === NOT_SURE_YET_GOAL) && - codingSkill !== null && - codingSkill >= 4 - ) { - nodeTypes = nodeTypes.concat(WEBHOOK_NODE_TYPE); - } else if ( - (companyType === PERSONAL_COMPANY_TYPE || - automationGoal === OTHER_AUTOMATION_GOAL || - automationGoal === NOT_SURE_YET_GOAL) && - codingSkill !== null && - codingSkill < 3 - ) { - nodeTypes = nodeTypes.concat(SCHEDULE_TRIGGER_NODE_TYPE); - } else if (automationGoal === CUSTOMER_INTEGRATIONS_GOAL) { - nodeTypes = nodeTypes.concat(WEBHOOK_NODE_TYPE); - } else if ( - automationGoal === CUSTOMER_SUPPORT_GOAL || - automationGoal === FINANCE_ACCOUNTING_GOAL - ) { - nodeTypes = nodeTypes.concat(ZENDESK_TRIGGER_NODE_TYPE); - } else if (automationGoal === SALES_MARKETING_GOAL) { - nodeTypes = nodeTypes.concat(HUBSPOT_TRIGGER_NODE_TYPE); - } else if (automationGoal === HR_GOAL) { - nodeTypes = nodeTypes.concat(WORKABLE_TRIGGER_NODE_TYPE); - } else if (automationGoal === OPERATIONS_GOAL) { - nodeTypes = nodeTypes.concat(SCHEDULE_TRIGGER_NODE_TYPE); - } else if (automationGoal === PRODUCT_GOAL) { - nodeTypes = nodeTypes.concat(NOTION_TRIGGER_NODE_TYPE); - } else if (automationGoal === SECURITY_GOAL) { - nodeTypes = nodeTypes.concat(THE_HIVE_TRIGGER_NODE_TYPE); - } else { - nodeTypes = nodeTypes.concat(WEBHOOK_NODE_TYPE); - } - - // slot 2 data transformation - if (codingSkill !== null && codingSkill >= 4) { - nodeTypes = nodeTypes.concat(CODE_NODE_TYPE); - } else { - nodeTypes = nodeTypes.concat(ITEM_LISTS_NODE_TYPE); - } - - // slot 3 logic node - if (codingSkill !== null && codingSkill < 3) { - nodeTypes = nodeTypes.concat(IF_NODE_TYPE); - } else { - nodeTypes = nodeTypes.concat(SWITCH_NODE_TYPE); - } - - // slot 4 use case #1 - if (companySize === COMPANY_SIZE_500_999 || companySize === COMPANY_SIZE_1000_OR_MORE) { - switch (automationGoal) { - case CUSTOMER_INTEGRATIONS_GOAL: - nodeTypes = nodeTypes.concat(HTTP_REQUEST_NODE_TYPE); - break; - case CUSTOMER_SUPPORT_GOAL: - nodeTypes = nodeTypes.concat(ZENDESK_NODE_TYPE); - break; - case SALES_MARKETING_GOAL: - nodeTypes = nodeTypes.concat(SALESFORCE_NODE_TYPE); - break; - case HR_GOAL: - nodeTypes = nodeTypes.concat(SERVICENOW_NODE_TYPE); - break; - case PRODUCT_GOAL: - nodeTypes = nodeTypes.concat(JIRA_NODE_TYPE); - break; - case FINANCE_ACCOUNTING_GOAL: - nodeTypes = nodeTypes.concat(SPREADSHEET_FILE_NODE_TYPE); - break; - case SECURITY_GOAL: - nodeTypes = nodeTypes.concat(ELASTIC_SECURITY_NODE_TYPE); - break; - default: - nodeTypes = nodeTypes.concat(SLACK_NODE_TYPE); - } - } else { - switch (automationGoal) { - case CUSTOMER_INTEGRATIONS_GOAL: - nodeTypes = nodeTypes.concat(HTTP_REQUEST_NODE_TYPE); - break; - case CUSTOMER_SUPPORT_GOAL: - nodeTypes = nodeTypes.concat(ZENDESK_NODE_TYPE); - break; - case FINANCE_ACCOUNTING_GOAL: - nodeTypes = nodeTypes.concat(QUICKBOOKS_NODE_TYPE); - break; - case HR_GOAL: - nodeTypes = nodeTypes.concat(BAMBOO_HR_NODE_TYPE); - break; - case PRODUCT_GOAL: - nodeTypes = nodeTypes.concat(JIRA_NODE_TYPE); - break; - case SALES_MARKETING_GOAL: - nodeTypes = nodeTypes.concat(GOOGLE_SHEETS_NODE_TYPE); - break; - case SECURITY_GOAL: - nodeTypes = nodeTypes.concat(ELASTIC_SECURITY_NODE_TYPE); - break; - default: - nodeTypes = nodeTypes.concat(SLACK_NODE_TYPE); - } - } - - // slot 4 - nodeTypes = nodeTypes.concat(SET_NODE_TYPE); - - return nodeTypes; -} - -function getPersonalizationSurveyV1(answers: IPersonalizationSurveyAnswersV1) { - const companySize = answers[COMPANY_SIZE_KEY]; - const workArea = answers[WORK_AREA_KEY]; - - function isWorkAreaAnswer(name: string) { - if (Array.isArray(workArea)) { - return workArea.includes(name); - } else { - return workArea === name; - } - } - - const workAreaIsEmpty = !workArea || workArea.length === 0; - - if (companySize === null && workAreaIsEmpty && answers[CODING_SKILL_KEY] === null) { - return []; - } - - let codingSkill = null; - if (answers[CODING_SKILL_KEY]) { - codingSkill = parseInt(answers[CODING_SKILL_KEY], 10); - codingSkill = isNaN(codingSkill) ? 0 : codingSkill; - } - - let nodeTypes = [] as string[]; - if (isWorkAreaAnswer(IT_ENGINEERING_WORK_AREA)) { - nodeTypes = nodeTypes.concat(WEBHOOK_NODE_TYPE); - } else { - nodeTypes = nodeTypes.concat(SCHEDULE_TRIGGER_NODE_TYPE); - } - - if (codingSkill !== null && codingSkill >= 4) { - nodeTypes = nodeTypes.concat(CODE_NODE_TYPE); - } else { - nodeTypes = nodeTypes.concat(ITEM_LISTS_NODE_TYPE); - } - - if (codingSkill !== null && codingSkill < 3) { - nodeTypes = nodeTypes.concat(IF_NODE_TYPE); - } else { - nodeTypes = nodeTypes.concat(SWITCH_NODE_TYPE); - } - - if (companySize === COMPANY_SIZE_500_999 || companySize === COMPANY_SIZE_1000_OR_MORE) { - if (isWorkAreaAnswer(SALES_BUSINESSDEV_WORK_AREA)) { - nodeTypes = nodeTypes.concat(SALESFORCE_NODE_TYPE); - } else if (isWorkAreaAnswer(SECURITY_WORK_AREA)) { - nodeTypes = nodeTypes.concat([ELASTIC_SECURITY_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]); - } else if (isWorkAreaAnswer(PRODUCT_WORK_AREA)) { - nodeTypes = nodeTypes.concat([JIRA_TRIGGER_NODE_TYPE, SEGMENT_NODE_TYPE]); - } else if (isWorkAreaAnswer(IT_ENGINEERING_WORK_AREA)) { - nodeTypes = nodeTypes.concat([GITHUB_TRIGGER_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]); - } else { - nodeTypes = nodeTypes.concat([MICROSOFT_EXCEL_NODE_TYPE, MICROSOFT_TEAMS_NODE_TYPE]); - } - } else { - if (isWorkAreaAnswer(SALES_BUSINESSDEV_WORK_AREA)) { - nodeTypes = nodeTypes.concat(CLEARBIT_NODE_TYPE); - } else if (isWorkAreaAnswer(SECURITY_WORK_AREA)) { - nodeTypes = nodeTypes.concat([PAGERDUTY_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]); - } else if (isWorkAreaAnswer(PRODUCT_WORK_AREA)) { - nodeTypes = nodeTypes.concat([JIRA_TRIGGER_NODE_TYPE, CALENDLY_TRIGGER_NODE_TYPE]); - } else if (isWorkAreaAnswer(IT_ENGINEERING_WORK_AREA)) { - nodeTypes = nodeTypes.concat([EXECUTE_COMMAND_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]); - } else if (isWorkAreaAnswer(FINANCE_WORK_AREA)) { - nodeTypes = nodeTypes.concat([ - XERO_NODE_TYPE, - QUICKBOOKS_NODE_TYPE, - SPREADSHEET_FILE_NODE_TYPE, - ]); - } else { - nodeTypes = nodeTypes.concat([EMAIL_SEND_NODE_TYPE, SLACK_NODE_TYPE]); - } - } - - nodeTypes = nodeTypes.concat(SET_NODE_TYPE); - - return nodeTypes; -}