From 2c9c3dab3360f6eee697a6571ce2de80e32f091c Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Tue, 3 Jun 2025 14:52:24 +0200 Subject: [PATCH] fix(editor): Add user role tooltip to personal settings page (#15941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Raúl Gómez Morales --- .../frontend/@n8n/i18n/src/locales/en.json | 6 +++ .../editor-ui/src/stores/cloudPlan.store.ts | 5 +- .../src/views/SettingsPersonalView.test.ts | 31 +++++++++++ .../src/views/SettingsPersonalView.vue | 52 +++++++++++++++++-- 4 files changed, 89 insertions(+), 5 deletions(-) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 3baf2a4a83..be7a012b63 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -147,6 +147,7 @@ "auth.newPassword": "New password", "auth.password": "Password", "auth.role": "Role", + "auth.roles.default": "Default", "auth.roles.member": "Member", "auth.roles.admin": "Admin", "auth.roles.owner": "Owner", @@ -1892,6 +1893,11 @@ "settings.personal.personalSettings": "Personal Settings", "settings.personal.personalSettingsUpdated": "Personal details updated", "settings.personal.personalSettingsUpdatedError": "Problem updating your details", + "settings.personal.role.tooltip.default": "Default role for new users", + "settings.personal.role.tooltip.member": "Create and manage own workflows and credentials", + "settings.personal.role.tooltip.admin": "Full access to manage workflows,tags, credentials, projects, users and more", + "settings.personal.role.tooltip.owner": "Manage everything{cloudAccess}", + "settings.personal.role.tooltip.cloud": " and access Cloud dashboard", "settings.personal.save": "Save", "settings.personal.security": "Security", "settings.signup.signUpInviterInfo": "{firstName} {lastName} has invited you to n8n", diff --git a/packages/frontend/editor-ui/src/stores/cloudPlan.store.ts b/packages/frontend/editor-ui/src/stores/cloudPlan.store.ts index f99a944baa..f67b099d79 100644 --- a/packages/frontend/editor-ui/src/stores/cloudPlan.store.ts +++ b/packages/frontend/editor-ui/src/stores/cloudPlan.store.ts @@ -47,9 +47,9 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => { return state.usage?.executions >= state.data?.monthlyExecutionsLimit; }); - const hasCloudPlan = computed(() => { + const hasCloudPlan = computed(() => { const cloudUserId = settingsStore.settings.n8nMetadata?.userId; - return hasPermission(['instanceOwner']) && settingsStore.isCloudDeployment && cloudUserId; + return hasPermission(['instanceOwner']) && settingsStore.isCloudDeployment && !!cloudUserId; }); const getUserCloudAccount = async () => { @@ -189,6 +189,7 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => { currentUsageData, trialExpired, allExecutionsUsed, + hasCloudPlan, generateCloudDashboardAutoLoginLink, initialize, getOwnerCurrentPlan, diff --git a/packages/frontend/editor-ui/src/views/SettingsPersonalView.test.ts b/packages/frontend/editor-ui/src/views/SettingsPersonalView.test.ts index 9c8a330f6e..a75ba342df 100644 --- a/packages/frontend/editor-ui/src/views/SettingsPersonalView.test.ts +++ b/packages/frontend/editor-ui/src/views/SettingsPersonalView.test.ts @@ -1,3 +1,4 @@ +import userEvent from '@testing-library/user-event'; import { createPinia } from 'pinia'; import { waitAllPromises } from '@/__tests__/utils'; import SettingsPersonalView from '@/views/SettingsPersonalView.vue'; @@ -7,11 +8,13 @@ import { createComponentRenderer } from '@/__tests__/render'; import { setupServer } from '@/__tests__/server'; import { ROLE } from '@/constants'; import { useUIStore } from '@/stores/ui.store'; +import { useCloudPlanStore } from '@/stores/cloudPlan.store'; let pinia: ReturnType; let settingsStore: ReturnType; let usersStore: ReturnType; let uiStore: ReturnType; +let cloudPlanStore: ReturnType; let server: ReturnType; const renderComponent = createComponentRenderer(SettingsPersonalView); @@ -40,6 +43,7 @@ describe('SettingsPersonalView', () => { settingsStore = useSettingsStore(pinia); usersStore = useUsersStore(pinia); uiStore = useUIStore(pinia); + cloudPlanStore = useCloudPlanStore(pinia); usersStore.usersById[currentUser.id] = currentUser; usersStore.currentUserId = currentUser.id; @@ -143,4 +147,31 @@ describe('SettingsPersonalView', () => { expect(queryByTestId('mfa-section')).not.toBeInTheDocument(); }); }); + + test.each([ + ['Default', ROLE.Default, false, 'Default role for new users'], + ['Member', ROLE.Member, false, 'Create and manage own workflows and credentials'], + [ + 'Admin', + ROLE.Admin, + false, + 'Full access to manage workflows,tags, credentials, projects, users and more', + ], + ['Owner', ROLE.Owner, false, 'Manage everything'], + ['Owner', ROLE.Owner, true, 'Manage everything and access Cloud dashboard'], + ])('should show %s user role information', async (label, role, hasCloudPlan, tooltipText) => { + vi.spyOn(cloudPlanStore, 'hasCloudPlan', 'get').mockReturnValue(hasCloudPlan); + vi.spyOn(usersStore, 'globalRoleName', 'get').mockReturnValue(role); + + const { queryByTestId, getByText } = renderComponent({ pinia }); + await waitAllPromises(); + + expect(queryByTestId('current-user-role')).toBeVisible(); + expect(queryByTestId('current-user-role')).toHaveTextContent(label); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await userEvent.hover(queryByTestId('current-user-role')!); + + expect(getByText(tooltipText)).toBeVisible(); + }); }); diff --git a/packages/frontend/editor-ui/src/views/SettingsPersonalView.vue b/packages/frontend/editor-ui/src/views/SettingsPersonalView.vue index d82867e9f4..aae879be08 100644 --- a/packages/frontend/editor-ui/src/views/SettingsPersonalView.vue +++ b/packages/frontend/editor-ui/src/views/SettingsPersonalView.vue @@ -3,16 +3,18 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'; import { useI18n } from '@n8n/i18n'; import { useToast } from '@/composables/useToast'; import { useDocumentTitle } from '@/composables/useDocumentTitle'; -import type { IFormInputs, IUser, ThemeOption } from '@/Interface'; +import type { IFormInputs, IRole, IUser, ThemeOption } from '@/Interface'; import { CHANGE_PASSWORD_MODAL_KEY, MFA_DOCS_URL, MFA_SETUP_MODAL_KEY, PROMPT_MFA_CODE_MODAL_KEY, + ROLE, } from '@/constants'; import { useUIStore } from '@/stores/ui.store'; import { useUsersStore } from '@/stores/users.store'; import { useSettingsStore } from '@/stores/settings.store'; +import { useCloudPlanStore } from '@/stores/cloudPlan.store'; import { createFormEventBus } from '@n8n/design-system/utils'; import type { MfaModalEvents } from '@/event-bus/mfa'; import { promptMfaCodeBus } from '@/event-bus/mfa'; @@ -28,6 +30,11 @@ type UserBasicDetailsWithMfa = UserBasicDetailsForm & { mfaCode?: string; }; +type RoleContent = { + name: string; + description: string; +}; + const i18n = useI18n(); const { showToast, showError } = useToast(); const documentTitle = useDocumentTitle(); @@ -55,6 +62,7 @@ const themeOptions = ref>([ const uiStore = useUIStore(); const usersStore = useUsersStore(); const settingsStore = useSettingsStore(); +const cloudPlanStore = useCloudPlanStore(); const currentUser = computed((): IUser | null => { return usersStore.currentUser; @@ -82,6 +90,33 @@ const hasAnyChanges = computed(() => { return hasAnyBasicInfoChanges.value || hasAnyPersonalisationChanges.value; }); +const roles = computed>(() => ({ + [ROLE.Default]: { + name: i18n.baseText('auth.roles.default'), + description: i18n.baseText('settings.personal.role.tooltip.default'), + }, + [ROLE.Member]: { + name: i18n.baseText('auth.roles.member'), + description: i18n.baseText('settings.personal.role.tooltip.member'), + }, + [ROLE.Admin]: { + name: i18n.baseText('auth.roles.admin'), + description: i18n.baseText('settings.personal.role.tooltip.admin'), + }, + [ROLE.Owner]: { + name: i18n.baseText('auth.roles.owner'), + description: i18n.baseText('settings.personal.role.tooltip.owner', { + interpolate: { + cloudAccess: cloudPlanStore.hasCloudPlan + ? i18n.baseText('settings.personal.role.tooltip.cloud') + : '', + }, + }), + }, +})); + +const currentUserRole = computed(() => roles.value[usersStore.globalRoleName]); + onMounted(() => { documentTitle.set(i18n.baseText('settings.personal.personalSettings')); formInputs.value = [ @@ -260,7 +295,13 @@ onBeforeUnmount(() => { }}
- {{ currentUser.fullName }} + {{ currentUser.fullName }} + + + {{ + currentUserRole.name + }} + { } .username { + display: grid; + grid-template-columns: 1fr; margin-right: var(--spacing-s); - text-align: right; @media (max-width: $breakpoint-sm) { max-width: 100px; @@ -405,6 +447,10 @@ onBeforeUnmount(() => { } } +.tooltip { + justify-self: start; +} + .disableMfaButton { --button-color: var(--color-danger); > span {