fix(editor): Open Community+ enrollment modal only for the instance owner (#11292)

This commit is contained in:
Csaba Tuncsik 2024-10-21 10:02:18 +02:00 committed by GitHub
parent cd15e959c7
commit 76724c3be6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 48 additions and 17 deletions

View file

@ -3,6 +3,7 @@ export const RESOURCES = {
annotationTag: [...DEFAULT_OPERATIONS] as const,
auditLogs: ['manage'] as const,
banner: ['dismiss'] as const,
community: ['register'] as const,
communityPackage: ['install', 'uninstall', 'update', 'list', 'manage'] as const,
credential: ['share', 'move', ...DEFAULT_OPERATIONS] as const,
externalSecretsProvider: ['sync', ...DEFAULT_OPERATIONS] as const,

View file

@ -13,6 +13,7 @@ export type WildcardScope = `${Resource}:*` | '*';
export type AnnotationTagScope = ResourceScope<'annotationTag'>;
export type AuditLogsScope = ResourceScope<'auditLogs', 'manage'>;
export type BannerScope = ResourceScope<'banner', 'dismiss'>;
export type CommunityScope = ResourceScope<'community', 'register'>;
export type CommunityPackageScope = ResourceScope<
'communityPackage',
'install' | 'uninstall' | 'update' | 'list' | 'manage'
@ -48,6 +49,7 @@ export type Scope =
| AnnotationTagScope
| AuditLogsScope
| BannerScope
| CommunityScope
| CommunityPackageScope
| CredentialScope
| ExternalSecretProviderScope

View file

@ -15,6 +15,7 @@ export const GLOBAL_OWNER_SCOPES: Scope[] = [
'credential:list',
'credential:share',
'credential:move',
'community:register',
'communityPackage:install',
'communityPackage:uninstall',
'communityPackage:update',

View file

@ -93,6 +93,7 @@ import { useExternalHooks } from '@/composables/useExternalHooks';
import { useI18n } from '@/composables/useI18n';
import { useRoute, useRouter } from 'vue-router';
import { useUIStore } from '@/stores/ui.store';
import { getResourcePermissions } from '@/permissions';
const SURVEY_VERSION = 'v4';
@ -110,7 +111,9 @@ const uiStore = useUIStore();
const formValues = ref<Record<string, string>>({});
const isSaving = ref(false);
const userPermissions = computed(() =>
getResourcePermissions(usersStore.currentUser?.globalScopes),
);
const survey = computed<IFormInputs>(() => [
{
name: COMPANY_TYPE_KEY,
@ -548,23 +551,30 @@ const onSave = () => {
formBus.emit('submit');
};
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) {
void router.replace({ name: VIEWS.HOMEPAGE });
}
};
const closeDialog = () => {
modalBus.emit('close');
uiStore.openModalWithData({
name: COMMUNITY_PLUS_ENROLLMENT_MODAL,
data: {
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) {
void router.replace({ name: VIEWS.HOMEPAGE });
}
if (userPermissions.value.community.register) {
uiStore.openModalWithData({
name: COMMUNITY_PLUS_ENROLLMENT_MODAL,
data: {
closeCallback,
},
},
});
});
} else {
closeCallback();
}
};
const onSubmit = async (values: IPersonalizationLatestVersion) => {

View file

@ -8,6 +8,7 @@ describe('permissions', () => {
annotationTag: {},
auditLogs: {},
banner: {},
community: {},
communityPackage: {},
credential: {},
externalSecretsProvider: {},
@ -62,6 +63,7 @@ describe('permissions', () => {
annotationTag: {},
auditLogs: {},
banner: {},
community: {},
communityPackage: {},
credential: {
create: true,

View file

@ -27,6 +27,7 @@ export const useRBACStore = defineStore(STORES.RBAC, () => {
eventBusDestination: {},
auditLogs: {},
banner: {},
community: {},
communityPackage: {},
ldap: {},
license: {},

View file

@ -6,6 +6,8 @@ import { useUsageStore } from '@/stores/usage.store';
import SettingsUsageAndPlan from '@/views/SettingsUsageAndPlan.vue';
import { useUIStore } from '@/stores/ui.store';
import { COMMUNITY_PLUS_ENROLLMENT_MODAL } from '@/constants';
import { useUsersStore } from '@/stores/users.store';
import type { IUser } from '@/Interface';
vi.mock('vue-router', () => {
return {
@ -23,6 +25,7 @@ vi.mock('vue-router', () => {
let usageStore: ReturnType<typeof mockedStore<typeof useUsageStore>>;
let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
let usersStore: ReturnType<typeof mockedStore<typeof useUsersStore>>;
const renderComponent = createComponentRenderer(SettingsUsageAndPlan);
@ -31,6 +34,7 @@ describe('SettingsUsageAndPlan', () => {
createTestingPinia();
usageStore = mockedStore(useUsageStore);
uiStore = mockedStore(useUIStore);
usersStore = mockedStore(useUsersStore);
usageStore.viewPlansUrl = 'https://subscription.n8n.io';
usageStore.managePlanUrl = 'https://subscription.n8n.io';
@ -49,6 +53,9 @@ describe('SettingsUsageAndPlan', () => {
it('should not show badge but unlock notice', async () => {
usageStore.isLoading = false;
usageStore.planName = 'Community';
usersStore.currentUser = {
globalScopes: ['community:register'],
} as IUser;
const { getByRole, container } = renderComponent();
expect(getByRole('heading', { level: 3 })).toHaveTextContent('Community');
expect(container.querySelector('.n8n-badge')).toBeNull();

View file

@ -11,11 +11,14 @@ import { useDocumentTitle } from '@/composables/useDocumentTitle';
import { hasPermission } from '@/utils/rbac/permissions';
import N8nInfoTip from 'n8n-design-system/components/N8nInfoTip';
import { COMMUNITY_PLUS_ENROLLMENT_MODAL } from '@/constants';
import { useUsersStore } from '@/stores/users.store';
import { getResourcePermissions } from '@/permissions';
const usageStore = useUsageStore();
const route = useRoute();
const router = useRouter();
const uiStore = useUIStore();
const usersStore = useUsersStore();
const toast = useToast();
const documentTitle = useDocumentTitle();
@ -48,6 +51,10 @@ const isCommunityEditionRegistered = computed(
() => usageStore.planName.toLowerCase() === 'registered community',
);
const canUserRegisterCommunityPlus = computed(
() => getResourcePermissions(usersStore.currentUser?.globalScopes).community.register,
);
const showActivationSuccess = () => {
toast.showMessage({
type: 'success',
@ -173,7 +180,7 @@ const openCommunityRegisterModal = () => {
</span>
</n8n-heading>
<N8nNotice v-if="isCommunity" class="mt-0" theme="warning">
<N8nNotice v-if="isCommunity && canUserRegisterCommunityPlus" class="mt-0" theme="warning">
<i18n-t keypath="settings.usageAndPlan.callOut">
<template #link>
<N8nButton

View file

@ -103,7 +103,7 @@ const onSubmit = async (values: { [key: string]: string | boolean }) => {
if (isPartOfOnboardingExperiment) {
await router.push({ name: VIEWS.WORKFLOWS });
} else {
await router.push({ name: VIEWS.NEW_WORKFLOW });
await router.push({ name: VIEWS.HOMEPAGE });
}
} else {
await router.push({ name: VIEWS.USERS_SETTINGS });