mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 12:57:29 -08:00
feat: Opt in to additional features on community for existing users (#11166)
This commit is contained in:
parent
c68782c633
commit
c2adfc8545
|
@ -14,8 +14,7 @@ type LicenseError = Error & { errorId?: keyof typeof LicenseErrors };
|
||||||
|
|
||||||
export const LicenseErrors = {
|
export const LicenseErrors = {
|
||||||
SCHEMA_VALIDATION: 'Activation key is in the wrong format',
|
SCHEMA_VALIDATION: 'Activation key is in the wrong format',
|
||||||
RESERVATION_EXHAUSTED:
|
RESERVATION_EXHAUSTED: 'Activation key has been used too many times',
|
||||||
'Activation key has been used too many times. Please contact sales@n8n.io if you would like to extend it',
|
|
||||||
RESERVATION_EXPIRED: 'Activation key has expired',
|
RESERVATION_EXPIRED: 'Activation key has expired',
|
||||||
NOT_FOUND: 'Activation key not found',
|
NOT_FOUND: 'Activation key not found',
|
||||||
RESERVATION_CONFLICT: 'Activation key not found',
|
RESERVATION_CONFLICT: 'Activation key not found',
|
||||||
|
|
|
@ -51,6 +51,14 @@ describe('CommunityPlusEnrollmentModal', () => {
|
||||||
createTestingPinia();
|
createTestingPinia();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not throw error opened only with the name', () => {
|
||||||
|
const props = {
|
||||||
|
modalName: COMMUNITY_PLUS_ENROLLMENT_MODAL,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(() => renderComponent({ props })).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
it('should test enrolling', async () => {
|
it('should test enrolling', async () => {
|
||||||
const closeCallbackSpy = vi.fn();
|
const closeCallbackSpy = vi.fn();
|
||||||
const usageStore = mockedStore(useUsageStore);
|
const usageStore = mockedStore(useUsageStore);
|
||||||
|
@ -168,4 +176,18 @@ describe('CommunityPlusEnrollmentModal', () => {
|
||||||
const emailInput = getByRole('textbox');
|
const emailInput = getByRole('textbox');
|
||||||
expect(emailInput).toHaveValue('test@n8n.io');
|
expect(emailInput).toHaveValue('test@n8n.io');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not throw error if no close callback provided', async () => {
|
||||||
|
const consoleErrorSpy = vi.spyOn(console, 'error');
|
||||||
|
const props = {
|
||||||
|
modalName: COMMUNITY_PLUS_ENROLLMENT_MODAL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { getByRole } = renderComponent({ props });
|
||||||
|
const skipButton = getByRole('button', { name: 'Skip' });
|
||||||
|
expect(skipButton).toBeVisible();
|
||||||
|
|
||||||
|
await userEvent.click(skipButton);
|
||||||
|
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,8 +13,8 @@ import { useUsersStore } from '@/stores/users.store';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modalName: string;
|
modalName: string;
|
||||||
data: {
|
data?: {
|
||||||
closeCallback: () => void;
|
closeCallback?: () => void;
|
||||||
};
|
};
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ const modalBus = createEventBus();
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
telemetry.track('User skipped community plus');
|
telemetry.track('User skipped community plus');
|
||||||
modalBus.emit('close');
|
modalBus.emit('close');
|
||||||
props.data.closeCallback();
|
props.data?.closeCallback?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirm = async () => {
|
const confirm = async () => {
|
||||||
|
|
|
@ -1788,6 +1788,8 @@
|
||||||
"settings.usageAndPlan.title": "Usage and plan",
|
"settings.usageAndPlan.title": "Usage and plan",
|
||||||
"settings.usageAndPlan.description": "You’re on the {name} {type}",
|
"settings.usageAndPlan.description": "You’re on the {name} {type}",
|
||||||
"settings.usageAndPlan.plan": "Plan",
|
"settings.usageAndPlan.plan": "Plan",
|
||||||
|
"settings.usageAndPlan.callOut": "{link} selected paid features for free (forever)",
|
||||||
|
"settings.usageAndPlan.callOut.link": "Unlock",
|
||||||
"settings.usageAndPlan.edition": "Edition",
|
"settings.usageAndPlan.edition": "Edition",
|
||||||
"settings.usageAndPlan.error": "@:_reusableBaseText.error",
|
"settings.usageAndPlan.error": "@:_reusableBaseText.error",
|
||||||
"settings.usageAndPlan.activeWorkflows": "Active workflows",
|
"settings.usageAndPlan.activeWorkflows": "Active workflows",
|
||||||
|
@ -2662,7 +2664,7 @@
|
||||||
"feedback.positive": "I found this helpful",
|
"feedback.positive": "I found this helpful",
|
||||||
"feedback.negative": "I didn't find this helpful",
|
"feedback.negative": "I didn't find this helpful",
|
||||||
"communityPlusModal.badge": "Time limited offer",
|
"communityPlusModal.badge": "Time limited offer",
|
||||||
"communityPlusModal.title": "Unlock select paid features for free (forever)",
|
"communityPlusModal.title": "Get paid features for free (forever)",
|
||||||
"communityPlusModal.error.title": "License request failed",
|
"communityPlusModal.error.title": "License request failed",
|
||||||
"communityPlusModal.success.title": "Request sent",
|
"communityPlusModal.success.title": "Request sent",
|
||||||
"communityPlusModal.success.message": "License key will be sent to {email}",
|
"communityPlusModal.success.message": "License key will be sent to {email}",
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { mockedStore } from '@/__tests__/utils';
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
import { useUsageStore } from '@/stores/usage.store';
|
import { useUsageStore } from '@/stores/usage.store';
|
||||||
import SettingsUsageAndPlan from '@/views/SettingsUsageAndPlan.vue';
|
import SettingsUsageAndPlan from '@/views/SettingsUsageAndPlan.vue';
|
||||||
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
|
import { COMMUNITY_PLUS_ENROLLMENT_MODAL } from '@/constants';
|
||||||
|
|
||||||
vi.mock('vue-router', () => {
|
vi.mock('vue-router', () => {
|
||||||
return {
|
return {
|
||||||
|
@ -19,6 +22,7 @@ vi.mock('vue-router', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
let usageStore: ReturnType<typeof mockedStore<typeof useUsageStore>>;
|
let usageStore: ReturnType<typeof mockedStore<typeof useUsageStore>>;
|
||||||
|
let uiStore: ReturnType<typeof mockedStore<typeof useUIStore>>;
|
||||||
|
|
||||||
const renderComponent = createComponentRenderer(SettingsUsageAndPlan);
|
const renderComponent = createComponentRenderer(SettingsUsageAndPlan);
|
||||||
|
|
||||||
|
@ -26,6 +30,10 @@ describe('SettingsUsageAndPlan', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createTestingPinia();
|
createTestingPinia();
|
||||||
usageStore = mockedStore(useUsageStore);
|
usageStore = mockedStore(useUsageStore);
|
||||||
|
uiStore = mockedStore(useUIStore);
|
||||||
|
|
||||||
|
usageStore.viewPlansUrl = 'https://subscription.n8n.io';
|
||||||
|
usageStore.managePlanUrl = 'https://subscription.n8n.io';
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not throw errors when rendering', async () => {
|
it('should not throw errors when rendering', async () => {
|
||||||
|
@ -38,10 +46,21 @@ describe('SettingsUsageAndPlan', () => {
|
||||||
expect(getByRole('heading').nextElementSibling).toBeNull();
|
expect(getByRole('heading').nextElementSibling).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not show badge but unlock notice', async () => {
|
||||||
|
usageStore.isLoading = false;
|
||||||
|
usageStore.planName = 'Community';
|
||||||
|
const { getByRole, container } = renderComponent();
|
||||||
|
expect(getByRole('heading', { level: 3 })).toHaveTextContent('Community');
|
||||||
|
expect(container.querySelector('.n8n-badge')).toBeNull();
|
||||||
|
|
||||||
|
expect(getByRole('button', { name: 'Unlock' })).toBeVisible();
|
||||||
|
|
||||||
|
await userEvent.click(getByRole('button', { name: 'Unlock' }));
|
||||||
|
expect(uiStore.openModal).toHaveBeenCalledWith(COMMUNITY_PLUS_ENROLLMENT_MODAL);
|
||||||
|
});
|
||||||
|
|
||||||
it('should show community registered badge', async () => {
|
it('should show community registered badge', async () => {
|
||||||
usageStore.isLoading = false;
|
usageStore.isLoading = false;
|
||||||
usageStore.viewPlansUrl = 'https://subscription.n8n.io';
|
|
||||||
usageStore.managePlanUrl = 'https://subscription.n8n.io';
|
|
||||||
usageStore.planName = 'Registered Community';
|
usageStore.planName = 'Registered Community';
|
||||||
const { getByRole, container } = renderComponent();
|
const { getByRole, container } = renderComponent();
|
||||||
expect(getByRole('heading', { level: 3 })).toHaveTextContent('Community Edition');
|
expect(getByRole('heading', { level: 3 })).toHaveTextContent('Community Edition');
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { useUIStore } from '@/stores/ui.store';
|
||||||
import { useToast } from '@/composables/useToast';
|
import { useToast } from '@/composables/useToast';
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
import { hasPermission } from '@/utils/rbac/permissions';
|
import { hasPermission } from '@/utils/rbac/permissions';
|
||||||
|
import N8nInfoTip from 'n8n-design-system/components/N8nInfoTip';
|
||||||
|
import { COMMUNITY_PLUS_ENROLLMENT_MODAL } from '@/constants';
|
||||||
|
|
||||||
const usageStore = useUsageStore();
|
const usageStore = useUsageStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
@ -40,6 +42,8 @@ const badgedPlanName = computed(() => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isCommunity = computed(() => usageStore.planName.toLowerCase() === 'community');
|
||||||
|
|
||||||
const isCommunityEditionRegistered = computed(
|
const isCommunityEditionRegistered = computed(
|
||||||
() => usageStore.planName.toLowerCase() === 'registered community',
|
() => usageStore.planName.toLowerCase() === 'registered community',
|
||||||
);
|
);
|
||||||
|
@ -133,6 +137,10 @@ const onDialogClosed = () => {
|
||||||
const onDialogOpened = () => {
|
const onDialogOpened = () => {
|
||||||
activationKeyInput.value?.focus();
|
activationKeyInput.value?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openCommunityRegisterModal = () => {
|
||||||
|
uiStore.openModal(COMMUNITY_PLUS_ENROLLMENT_MODAL);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -143,9 +151,7 @@ const onDialogOpened = () => {
|
||||||
<div v-if="!usageStore.isLoading">
|
<div v-if="!usageStore.isLoading">
|
||||||
<n8n-heading tag="h3" :class="$style.title" size="large">
|
<n8n-heading tag="h3" :class="$style.title" size="large">
|
||||||
<i18n-t keypath="settings.usageAndPlan.description" tag="span">
|
<i18n-t keypath="settings.usageAndPlan.description" tag="span">
|
||||||
<template #name>{{
|
<template #name>{{ badgedPlanName.name ?? usageStore.planName }}</template>
|
||||||
badgedPlanName.badge ? badgedPlanName.name : usageStore.planName
|
|
||||||
}}</template>
|
|
||||||
<template #type>
|
<template #type>
|
||||||
<span v-if="usageStore.planId">{{
|
<span v-if="usageStore.planId">{{
|
||||||
locale.baseText('settings.usageAndPlan.plan')
|
locale.baseText('settings.usageAndPlan.plan')
|
||||||
|
@ -153,8 +159,8 @@ const onDialogOpened = () => {
|
||||||
<span v-else>{{ locale.baseText('settings.usageAndPlan.edition') }}</span>
|
<span v-else>{{ locale.baseText('settings.usageAndPlan.edition') }}</span>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
<span :class="$style.titleTooltip">
|
<span v-if="badgedPlanName.badge && badgedPlanName.name" :class="$style.titleTooltip">
|
||||||
<N8nTooltip v-if="badgedPlanName.badge" placement="top">
|
<N8nTooltip placement="top">
|
||||||
<template #content>
|
<template #content>
|
||||||
<i18n-t
|
<i18n-t
|
||||||
v-if="isCommunityEditionRegistered"
|
v-if="isCommunityEditionRegistered"
|
||||||
|
@ -167,6 +173,19 @@ const onDialogOpened = () => {
|
||||||
</span>
|
</span>
|
||||||
</n8n-heading>
|
</n8n-heading>
|
||||||
|
|
||||||
|
<N8nNotice v-if="isCommunity" class="mt-0" theme="warning">
|
||||||
|
<i18n-t keypath="settings.usageAndPlan.callOut">
|
||||||
|
<template #link>
|
||||||
|
<N8nButton
|
||||||
|
class="pl-0 pr-0"
|
||||||
|
text
|
||||||
|
:label="locale.baseText('settings.usageAndPlan.callOut.link')"
|
||||||
|
@click="openCommunityRegisterModal"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</N8nNotice>
|
||||||
|
|
||||||
<div :class="$style.quota">
|
<div :class="$style.quota">
|
||||||
<n8n-text size="medium" color="text-light">
|
<n8n-text size="medium" color="text-light">
|
||||||
{{ locale.baseText('settings.usageAndPlan.activeWorkflows') }}
|
{{ locale.baseText('settings.usageAndPlan.activeWorkflows') }}
|
||||||
|
@ -194,9 +213,7 @@ const onDialogOpened = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n8n-info-tip>{{
|
<N8nInfoTip>{{ locale.baseText('settings.usageAndPlan.activeWorkflows.hint') }}</N8nInfoTip>
|
||||||
locale.baseText('settings.usageAndPlan.activeWorkflows.hint')
|
|
||||||
}}</n8n-info-tip>
|
|
||||||
|
|
||||||
<div :class="$style.buttons">
|
<div :class="$style.buttons">
|
||||||
<n8n-button
|
<n8n-button
|
||||||
|
|
Loading…
Reference in a new issue