mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(editor): Add user role tooltip to personal settings page (#15941)
Co-authored-by: Raúl Gómez Morales <raul00gm@gmail.com>
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -47,9 +47,9 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
|
||||
return state.usage?.executions >= state.data?.monthlyExecutionsLimit;
|
||||
});
|
||||
|
||||
const hasCloudPlan = computed(() => {
|
||||
const hasCloudPlan = computed<boolean>(() => {
|
||||
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,
|
||||
|
||||
@@ -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<typeof createPinia>;
|
||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
let usersStore: ReturnType<typeof useUsersStore>;
|
||||
let uiStore: ReturnType<typeof useUIStore>;
|
||||
let cloudPlanStore: ReturnType<typeof useCloudPlanStore>;
|
||||
let server: ReturnType<typeof setupServer>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Array<{ name: ThemeOption; label: BaseTextKey }>>([
|
||||
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<Record<IRole, RoleContent>>(() => ({
|
||||
[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<RoleContent>(() => roles.value[usersStore.globalRoleName]);
|
||||
|
||||
onMounted(() => {
|
||||
documentTitle.set(i18n.baseText('settings.personal.personalSettings'));
|
||||
formInputs.value = [
|
||||
@@ -260,7 +295,13 @@ onBeforeUnmount(() => {
|
||||
}}</n8n-heading>
|
||||
<div v-if="currentUser" :class="$style.user">
|
||||
<span :class="$style.username" data-test-id="current-user-name">
|
||||
<n8n-text color="text-light">{{ currentUser.fullName }}</n8n-text>
|
||||
<n8n-text color="text-base" bold>{{ currentUser.fullName }}</n8n-text>
|
||||
<N8nTooltip placement="bottom">
|
||||
<template #content>{{ currentUserRole.description }}</template>
|
||||
<n8n-text :class="$style.tooltip" color="text-light" data-test-id="current-user-role">{{
|
||||
currentUserRole.name
|
||||
}}</n8n-text>
|
||||
</N8nTooltip>
|
||||
</span>
|
||||
<n8n-avatar
|
||||
:first-name="currentUser.firstName"
|
||||
@@ -395,8 +436,9 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
||||
Reference in New Issue
Block a user