+
@@ -102,6 +103,15 @@ const infoUrl = computed(() => {
div {
padding-top: 20px;
}
+
+ .link {
+ padding-left: 0px;
+ }
+
+ .link:hover {
+ color: var(--prim-color-primary);
+ text-decoration: none;
+ }
}
.versions {
diff --git a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue
index 886bd90003e11..d46c1df3b14d7 100644
--- a/packages/editor-ui/src/components/WorkflowShareModal.ee.vue
+++ b/packages/editor-ui/src/components/WorkflowShareModal.ee.vue
@@ -30,6 +30,7 @@ import type { ProjectListItem, ProjectSharingData, Project } from '@/types/proje
import { ProjectTypes } from '@/types/projects.types';
import { useRolesStore } from '@/stores/roles.store';
import type { RoleMap } from '@/types/roles.types';
+import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
export default defineComponent({
name: 'WorkflowShareModal',
@@ -47,6 +48,7 @@ export default defineComponent({
return {
...useToast(),
...useMessage(),
+ ...usePageRedirectionHelper(),
};
},
data() {
@@ -235,7 +237,7 @@ export default defineComponent({
});
},
goToUpgrade() {
- void this.uiStore.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing');
+ void this.goToUpgrade('workflow_sharing', 'upgrade-workflow-sharing');
},
async initialize() {
if (this.isSharingEnabled) {
diff --git a/packages/editor-ui/src/components/banners/TrialBanner.vue b/packages/editor-ui/src/components/banners/TrialBanner.vue
index f09a561df5c37..25c0f3d802748 100644
--- a/packages/editor-ui/src/components/banners/TrialBanner.vue
+++ b/packages/editor-ui/src/components/banners/TrialBanner.vue
@@ -3,13 +3,15 @@ import BaseBanner from '@/components/banners/BaseBanner.vue';
import { i18n as locale } from '@/plugins/i18n';
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { computed } from 'vue';
-import { useUIStore } from '@/stores/ui.store';
import type { CloudPlanAndUsageData } from '@/Interface';
+import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
const PROGRESS_BAR_MINIMUM_THRESHOLD = 8;
const cloudPlanStore = useCloudPlanStore();
+const pageRedirectionHelper = usePageRedirectionHelper();
+
const trialDaysLeft = computed(() => -1 * cloudPlanStore.trialDaysLeft);
const messageText = computed(() => {
return locale.baseText('banners.trial.message', {
@@ -49,7 +51,7 @@ const currentExecutions = computed(() => {
});
function onUpdatePlanClick() {
- void useUIStore().goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
+ void pageRedirectionHelper.goToUpgrade('canvas-nav', 'upgrade-canvas-nav', 'redirect');
}
diff --git a/packages/editor-ui/src/components/banners/TrialOverBanner.vue b/packages/editor-ui/src/components/banners/TrialOverBanner.vue
index 9080ad0fed9d3..45eedf9c14205 100644
--- a/packages/editor-ui/src/components/banners/TrialOverBanner.vue
+++ b/packages/editor-ui/src/components/banners/TrialOverBanner.vue
@@ -1,10 +1,10 @@
diff --git a/packages/editor-ui/src/components/executions/ExecutionsFilter.vue b/packages/editor-ui/src/components/executions/ExecutionsFilter.vue
index b76b51f24defb..4325c9b94fad1 100644
--- a/packages/editor-ui/src/components/executions/ExecutionsFilter.vue
+++ b/packages/editor-ui/src/components/executions/ExecutionsFilter.vue
@@ -10,11 +10,11 @@ import { i18n as locale } from '@/plugins/i18n';
import { getObjectKeys, isEmpty } from '@/utils/typesUtils';
import { EnterpriseEditionFeature } from '@/constants';
import { useSettingsStore } from '@/stores/settings.store';
-import { useUIStore } from '@/stores/ui.store';
import { useTelemetry } from '@/composables/useTelemetry';
import type { Placement } from '@floating-ui/core';
import { useDebounce } from '@/composables/useDebounce';
import AnnotationTagsDropdown from '@/components/AnnotationTagsDropdown.ee.vue';
+import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
export type ExecutionFilterProps = {
workflows?: Array
;
@@ -25,10 +25,10 @@ export type ExecutionFilterProps = {
const DATE_TIME_MASK = 'YYYY-MM-DD HH:mm';
const settingsStore = useSettingsStore();
-const uiStore = useUIStore();
const { debounce } = useDebounce();
const telemetry = useTelemetry();
+const pageRedirectionHelper = usePageRedirectionHelper();
const props = withDefaults(defineProps(), {
workflows: () => [] as Array,
@@ -149,7 +149,7 @@ const onFilterReset = () => {
};
const goToUpgrade = () => {
- void uiStore.goToUpgrade('custom-data-filter', 'upgrade-custom-data-filter');
+ void pageRedirectionHelper.goToUpgrade('custom-data-filter', 'upgrade-custom-data-filter');
};
onBeforeMount(() => {
diff --git a/packages/editor-ui/src/composables/__tests__/usePageRedirectionHelper.test.ts b/packages/editor-ui/src/composables/__tests__/usePageRedirectionHelper.test.ts
new file mode 100644
index 0000000000000..ee080531dea11
--- /dev/null
+++ b/packages/editor-ui/src/composables/__tests__/usePageRedirectionHelper.test.ts
@@ -0,0 +1,231 @@
+import { ROLE } from '@/constants';
+import { useSettingsStore } from '@/stores/settings.store';
+import { merge } from 'lodash-es';
+import { usePageRedirectionHelper } from '../usePageRedirectionHelper';
+import { defaultSettings } from '@/__tests__/defaults';
+import { useUsersStore } from '@/stores/users.store';
+import { createPinia, setActivePinia } from 'pinia';
+import * as cloudPlanApi from '@/api/cloudPlans';
+import { useVersionsStore } from '@/stores/versions.store';
+import { useTelemetry } from '../useTelemetry';
+
+let settingsStore: ReturnType;
+let usersStore: ReturnType;
+let versionStore: ReturnType;
+let pageRedirectionHelper: ReturnType;
+
+vi.mock('@/composables/useTelemetry', () => {
+ const track = vi.fn();
+ return {
+ useTelemetry: () => {
+ return {
+ track,
+ };
+ },
+ };
+});
+
+describe('usePageRedirectionHelper', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ beforeEach(() => {
+ setActivePinia(createPinia());
+ settingsStore = useSettingsStore();
+ usersStore = useUsersStore();
+ versionStore = useVersionsStore();
+
+ pageRedirectionHelper = usePageRedirectionHelper();
+
+ vi.spyOn(cloudPlanApi, 'getAdminPanelLoginCode').mockResolvedValue({
+ code: '123',
+ });
+
+ const url = 'https://test.app.n8n.cloud';
+
+ Object.defineProperty(window, 'location', {
+ value: {
+ href: url,
+ },
+ writable: true,
+ });
+
+ versionStore.setVersionNotificationSettings({
+ enabled: true,
+ endpoint: '',
+ infoUrl:
+ 'https://docs.n8n.io/release-notes/#n8n1652?utm_source=n8n_app&utm_medium=instance_upgrade_releases',
+ });
+ });
+
+ test.each([
+ [
+ 'default',
+ 'production',
+ ROLE.Owner,
+ 'https://n8n.io/pricing?utm_campaign=upgrade-api&source=advanced-permissions',
+ ],
+ [
+ 'default',
+ 'development',
+ ROLE.Owner,
+ 'https://n8n.io/pricing?utm_campaign=upgrade-api&source=advanced-permissions',
+ ],
+ [
+ 'cloud',
+ 'production',
+ ROLE.Owner,
+ `https://app.n8n.cloud/login?code=123&returnPath=${encodeURIComponent(
+ '/account/change-plan',
+ )}&utm_campaign=upgrade-api&source=advanced-permissions`,
+ ],
+ [
+ 'cloud',
+ 'production',
+ ROLE.Member,
+ 'https://n8n.io/pricing?utm_campaign=upgrade-api&source=advanced-permissions',
+ ],
+ ])(
+ '"goToUpgrade" should generate the correct URL for "%s" deployment and "%s" license environment and user role "%s"',
+ async (type, environment, role, expectation) => {
+ // Arrange
+
+ usersStore.addUsers([
+ {
+ id: '1',
+ isPending: false,
+ role,
+ },
+ ]);
+
+ usersStore.currentUserId = '1';
+
+ const telemetry = useTelemetry();
+
+ settingsStore.setSettings(
+ merge({}, defaultSettings, {
+ deployment: {
+ type,
+ },
+ license: {
+ environment,
+ },
+ }),
+ );
+
+ // Act
+
+ await pageRedirectionHelper.goToUpgrade('advanced-permissions', 'upgrade-api', 'redirect');
+
+ // Assert
+
+ expect(telemetry.track).toHaveBeenCalledWith(
+ 'User clicked upgrade CTA',
+ expect.objectContaining({
+ source: 'advanced-permissions',
+ isTrial: false,
+ deploymentType: type,
+ trialDaysLeft: expect.any(Number),
+ executionsLeft: expect.any(Number),
+ workflowsLeft: expect.any(Number),
+ }),
+ );
+
+ expect(location.href).toBe(expectation);
+ },
+ );
+
+ test.each([
+ [
+ 'cloud',
+ 'production',
+ ROLE.Owner,
+ `https://app.n8n.cloud/login?code=123&returnPath=${encodeURIComponent('/manage')}`,
+ ],
+ [
+ 'cloud',
+ 'production',
+ ROLE.Member,
+ 'https://docs.n8n.io/release-notes/#n8n1652?utm_source=n8n_app&utm_medium=instance_upgrade_releases',
+ ],
+ ])(
+ '"goToVersions" should generate the correct URL for "%s" deployment and "%s" license environment and user role "%s"',
+ async (type, environment, role, expectation) => {
+ // Arrange
+
+ usersStore.addUsers([
+ {
+ id: '1',
+ isPending: false,
+ role,
+ },
+ ]);
+
+ usersStore.currentUserId = '1';
+
+ settingsStore.setSettings(
+ merge({}, defaultSettings, {
+ deployment: {
+ type,
+ },
+ license: {
+ environment,
+ },
+ }),
+ );
+
+ // Act
+
+ await pageRedirectionHelper.goToVersions();
+
+ // Assert
+
+ expect(location.href).toBe(expectation);
+ },
+ );
+
+ test.each([
+ [
+ 'cloud',
+ 'production',
+ ROLE.Owner,
+ `https://app.n8n.cloud/login?code=123&returnPath=${encodeURIComponent('/dashboard')}`,
+ ],
+ ['cloud', 'production', ROLE.Member, 'https://test.app.n8n.cloud'],
+ ])(
+ '"goToDashboard" should generate the correct URL for "%s" deployment and "%s" license environment and user role "%s"',
+ async (type, environment, role, expectation) => {
+ // Arrange
+
+ usersStore.addUsers([
+ {
+ id: '1',
+ isPending: false,
+ role,
+ },
+ ]);
+
+ usersStore.currentUserId = '1';
+
+ settingsStore.setSettings(
+ merge({}, defaultSettings, {
+ deployment: {
+ type,
+ },
+ license: {
+ environment,
+ },
+ }),
+ );
+
+ // Act
+
+ await pageRedirectionHelper.goToDashboard();
+
+ // Assert
+
+ expect(location.href).toBe(expectation);
+ },
+ );
+});
diff --git a/packages/editor-ui/src/composables/useExecutionDebugging.ts b/packages/editor-ui/src/composables/useExecutionDebugging.ts
index cf59964bb8706..90f2a32cae907 100644
--- a/packages/editor-ui/src/composables/useExecutionDebugging.ts
+++ b/packages/editor-ui/src/composables/useExecutionDebugging.ts
@@ -17,6 +17,7 @@ import { useTelemetry } from './useTelemetry';
import { useRootStore } from '@/stores/root.store';
import { isFullExecutionResponse } from '@/utils/typeGuards';
import { sanitizeHtml } from '@/utils/htmlUtils';
+import { usePageRedirectionHelper } from './usePageRedirectionHelper';
export const useExecutionDebugging = () => {
const telemetry = useTelemetry();
@@ -29,6 +30,8 @@ export const useExecutionDebugging = () => {
const settingsStore = useSettingsStore();
const uiStore = useUIStore();
+ const pageRedirectionHelper = usePageRedirectionHelper();
+
const isDebugEnabled = computed(
() => settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.DebugInEditor],
);
@@ -147,7 +150,7 @@ export const useExecutionDebugging = () => {
title: i18n.baseText(uiStore.contextBasedTranslationKeys.feature.unavailable.title),
footerButtonAction: () => {
uiStore.closeModal(DEBUG_PAYWALL_MODAL_KEY);
- void uiStore.goToUpgrade('debug', 'upgrade-debug');
+ void pageRedirectionHelper.goToUpgrade('debug', 'upgrade-debug');
},
},
});
diff --git a/packages/editor-ui/src/composables/usePageRedirectionHelper.ts b/packages/editor-ui/src/composables/usePageRedirectionHelper.ts
new file mode 100644
index 0000000000000..42a1616dd98ae
--- /dev/null
+++ b/packages/editor-ui/src/composables/usePageRedirectionHelper.ts
@@ -0,0 +1,105 @@
+import { useUsersStore } from '@/stores/users.store';
+import { useCloudPlanStore } from '@/stores/cloudPlan.store';
+import { useVersionsStore } from '@/stores/versions.store';
+import { useTelemetry } from './useTelemetry';
+import { useSettingsStore } from '@/stores/settings.store';
+import type { CloudUpdateLinkSourceType, UTMCampaign } from '@/Interface';
+import { N8N_PRICING_PAGE_URL } from '@/constants';
+
+export function usePageRedirectionHelper() {
+ const usersStore = useUsersStore();
+ const cloudPlanStore = useCloudPlanStore();
+ const versionsStore = useVersionsStore();
+ const telemetry = useTelemetry();
+ const settingsStore = useSettingsStore();
+
+ /**
+ * If the user is an instance owner in the cloud, it generates an auto-login link to the
+ * cloud dashboard that redirects the user to the /manage page where they can upgrade to a new n8n version.
+ * Otherwise, it redirect them to our docs.
+ */
+ const goToVersions = async () => {
+ let versionsLink = versionsStore.infoUrl;
+
+ if (usersStore.isInstanceOwner && settingsStore.isCloudDeployment) {
+ versionsLink = await cloudPlanStore.generateCloudDashboardAutoLoginLink({
+ redirectionPath: '/manage',
+ });
+ }
+
+ location.href = versionsLink;
+ };
+
+ const goToDashboard = async () => {
+ if (usersStore.isInstanceOwner && settingsStore.isCloudDeployment) {
+ const dashboardLink = await cloudPlanStore.generateCloudDashboardAutoLoginLink({
+ redirectionPath: '/dashboard',
+ });
+
+ location.href = dashboardLink;
+ }
+
+ return;
+ };
+
+ /**
+ * If the user is an instance owner in the cloud, it generates an auto-login link to the
+ * cloud dashboard that redirects the user to the /account/change-plan page where they upgrade/downgrade the current plan.
+ * Otherwise, it redirect them our website.
+ */
+
+ const goToUpgrade = async (
+ source: CloudUpdateLinkSourceType,
+ utm_campaign: UTMCampaign,
+ mode: 'open' | 'redirect' = 'open',
+ ) => {
+ const { usageLeft, trialDaysLeft, userIsTrialing } = cloudPlanStore;
+ const { executionsLeft, workflowsLeft } = usageLeft;
+ const deploymentType = settingsStore.deploymentType;
+
+ telemetry.track('User clicked upgrade CTA', {
+ source,
+ isTrial: userIsTrialing,
+ deploymentType,
+ trialDaysLeft,
+ executionsLeft,
+ workflowsLeft,
+ });
+
+ const upgradeLink = await generateUpgradeLink(source, utm_campaign);
+
+ if (mode === 'open') {
+ window.open(upgradeLink, '_blank');
+ } else {
+ location.href = upgradeLink;
+ }
+ };
+
+ const generateUpgradeLink = async (source: string, utm_campaign: string) => {
+ let upgradeLink = N8N_PRICING_PAGE_URL;
+
+ if (usersStore.isInstanceOwner && settingsStore.isCloudDeployment) {
+ upgradeLink = await cloudPlanStore.generateCloudDashboardAutoLoginLink({
+ redirectionPath: '/account/change-plan',
+ });
+ }
+
+ const url = new URL(upgradeLink);
+
+ if (utm_campaign) {
+ url.searchParams.set('utm_campaign', utm_campaign);
+ }
+
+ if (source) {
+ url.searchParams.set('source', source);
+ }
+
+ return url.toString();
+ };
+
+ return {
+ goToDashboard,
+ goToVersions,
+ goToUpgrade,
+ };
+}
diff --git a/packages/editor-ui/src/stores/__tests__/ui.test.ts b/packages/editor-ui/src/stores/__tests__/ui.test.ts
index ace9ab3bca474..7de20d2502594 100644
--- a/packages/editor-ui/src/stores/__tests__/ui.test.ts
+++ b/packages/editor-ui/src/stores/__tests__/ui.test.ts
@@ -1,5 +1,5 @@
import { createPinia, setActivePinia } from 'pinia';
-import { generateUpgradeLinkUrl, useUIStore } from '@/stores/ui.store';
+import { useUIStore } from '@/stores/ui.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';
import { merge } from 'lodash-es';
@@ -70,57 +70,6 @@ describe('UI store', () => {
});
});
- test.each([
- [
- 'default',
- 'production',
- ROLE.Owner,
- 'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source',
- ],
- [
- 'default',
- 'development',
- ROLE.Owner,
- 'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source',
- ],
- [
- 'cloud',
- 'production',
- ROLE.Owner,
- `https://app.n8n.cloud/login?code=123&returnPath=${encodeURIComponent(
- '/account/change-plan',
- )}&utm_campaign=utm-test-campaign&source=test_source`,
- ],
- [
- 'cloud',
- 'production',
- ROLE.Member,
- 'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source',
- ],
- ])(
- '"generateUpgradeLinkUrl" should generate the correct URL for "%s" deployment and "%s" license environment and user role "%s"',
- async (type, environment, role, expectation) => {
- setUser(role as IRole);
-
- settingsStore.setSettings(
- merge({}, defaultSettings, {
- deployment: {
- type,
- },
- license: {
- environment,
- },
- instanceId: '123abc',
- versionCli: '0.223.0',
- }),
- );
-
- const updateLinkUrl = await generateUpgradeLinkUrl('test_source', 'utm-test-campaign', type);
-
- expect(updateLinkUrl).toBe(expectation);
- },
- );
-
it('should add non-production license banner to stack based on enterprise settings', () => {
settingsStore.setSettings(
merge({}, defaultSettings, {
diff --git a/packages/editor-ui/src/stores/cloudPlan.store.ts b/packages/editor-ui/src/stores/cloudPlan.store.ts
index 6d14383fb43e3..4f8c29ebef2d7 100644
--- a/packages/editor-ui/src/stores/cloudPlan.store.ts
+++ b/packages/editor-ui/src/stores/cloudPlan.store.ts
@@ -147,12 +147,6 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
}
};
- const redirectToDashboard = async () => {
- const adminPanelHost = new URL(window.location.href).host.split('.').slice(1).join('.');
- const { code } = await getAutoLoginCode();
- window.location.href = `https://${adminPanelHost}/login?code=${code}`;
- };
-
const initialize = async () => {
if (state.initialized) {
return;
@@ -173,11 +167,22 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
state.initialized = true;
};
+ const generateCloudDashboardAutoLoginLink = async (data: {
+ redirectionPath: string;
+ }) => {
+ const searchParams = new URLSearchParams();
+
+ const adminPanelHost = new URL(window.location.href).host.split('.').slice(1).join('.');
+ const { code } = await getAutoLoginCode();
+ const linkUrl = `https://${adminPanelHost}/login`;
+ searchParams.set('code', code);
+ searchParams.set('returnPath', data.redirectionPath);
+
+ return `${linkUrl}?${searchParams.toString()}`;
+ };
+
return {
state,
- initialize,
- getOwnerCurrentPlan,
- getInstanceCurrentUsage,
usageLeft,
trialDaysLeft,
userIsTrialing,
@@ -185,10 +190,13 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
currentUsageData,
trialExpired,
allExecutionsUsed,
+ generateCloudDashboardAutoLoginLink,
+ initialize,
+ getOwnerCurrentPlan,
+ getInstanceCurrentUsage,
reset,
checkForCloudPlanData,
fetchUserCloudAccount,
getAutoLoginCode,
- redirectToDashboard,
};
});
diff --git a/packages/editor-ui/src/stores/settings.store.ts b/packages/editor-ui/src/stores/settings.store.ts
index 119fc20ac1039..7d881b74d4d9d 100644
--- a/packages/editor-ui/src/stores/settings.store.ts
+++ b/packages/editor-ui/src/stores/settings.store.ts
@@ -147,7 +147,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, () => {
const permanentlyDismissedBanners = computed(() => settings.value.banners?.dismissed ?? []);
const isBelowUserQuota = computed(
- () =>
+ (): boolean =>
userManagement.value.quota === -1 ||
userManagement.value.quota > useUsersStore().allUsers.length,
);
diff --git a/packages/editor-ui/src/stores/ui.store.ts b/packages/editor-ui/src/stores/ui.store.ts
index 89b581a6a40b1..6a12495543764 100644
--- a/packages/editor-ui/src/stores/ui.store.ts
+++ b/packages/editor-ui/src/stores/ui.store.ts
@@ -30,7 +30,6 @@ import {
SOURCE_CONTROL_PUSH_MODAL_KEY,
SOURCE_CONTROL_PULL_MODAL_KEY,
DEBUG_PAYWALL_MODAL_KEY,
- N8N_PRICING_PAGE_URL,
WORKFLOW_HISTORY_VERSION_RESTORE,
SETUP_CREDENTIALS_MODAL_KEY,
PROJECT_MOVE_RESOURCE_MODAL,
@@ -39,9 +38,7 @@ import {
COMMUNITY_PLUS_ENROLLMENT_MODAL,
} from '@/constants';
import type {
- CloudUpdateLinkSourceType,
INodeUi,
- UTMCampaign,
XYPosition,
Modals,
NewCredentialsModal,
@@ -53,10 +50,8 @@ import type {
import { defineStore } from 'pinia';
import { useRootStore } from '@/stores/root.store';
import * as curlParserApi from '@/api/curlHelper';
-import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useSettingsStore } from '@/stores/settings.store';
-import { hasPermission } from '@/utils/rbac/permissions';
import { useUsersStore } from '@/stores/users.store';
import { dismissBannerPermanently } from '@/api/ui';
import type { BannerName } from 'n8n-workflow';
@@ -69,9 +64,9 @@ import {
} from './ui.utils';
import { computed, ref } from 'vue';
import type { Connection } from '@vue-flow/core';
-import { useTelemetry } from '@/composables/useTelemetry';
let savedTheme: ThemeOption = 'system';
+
try {
const value = getThemeOverride();
if (isValidTheme(value)) {
@@ -189,8 +184,6 @@ export const useUIStore = defineStore(STORES.UI, () => {
const settingsStore = useSettingsStore();
const workflowsStore = useWorkflowsStore();
const rootStore = useRootStore();
- const telemetry = useTelemetry();
- const cloudPlanStore = useCloudPlanStore();
const userStore = useUsersStore();
const appliedTheme = computed(() => {
@@ -525,33 +518,6 @@ export const useUIStore = defineStore(STORES.UI, () => {
return parameters;
};
- const goToUpgrade = async (
- source: CloudUpdateLinkSourceType,
- utm_campaign: UTMCampaign,
- mode: 'open' | 'redirect' = 'open',
- ) => {
- const { usageLeft, trialDaysLeft, userIsTrialing } = cloudPlanStore;
- const { executionsLeft, workflowsLeft } = usageLeft;
- const deploymentType = settingsStore.deploymentType;
-
- telemetry.track('User clicked upgrade CTA', {
- source,
- isTrial: userIsTrialing,
- deploymentType,
- trialDaysLeft,
- executionsLeft,
- workflowsLeft,
- });
-
- const upgradeLink = await generateUpgradeLinkUrl(source, utm_campaign, deploymentType);
-
- if (mode === 'open') {
- window.open(upgradeLink, '_blank');
- } else {
- location.href = upgradeLink;
- }
- };
-
const removeBannerFromStack = (name: BannerName) => {
bannerStack.value = bannerStack.value.filter((bannerName) => bannerName !== name);
};
@@ -633,6 +599,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
currentView,
isAnyModalOpen,
pendingNotificationsForViews,
+ activeModals,
setTheme,
setMode,
setActiveId,
@@ -659,7 +626,6 @@ export const useUIStore = defineStore(STORES.UI, () => {
setCurlCommand,
toggleSidebarMenuCollapse,
getCurlToJson,
- goToUpgrade,
removeBannerFromStack,
dismissBanner,
updateBannersHeight,
@@ -668,7 +634,6 @@ export const useUIStore = defineStore(STORES.UI, () => {
setNotificationsForView,
deleteNotificationsForView,
resetLastInteractedWith,
- activeModals,
};
});
@@ -712,34 +677,3 @@ export const listenForModalChanges = (opts: {
});
});
};
-
-export const generateUpgradeLinkUrl = async (
- source: string,
- utm_campaign: string,
- deploymentType: string,
-) => {
- let linkUrl = '';
-
- const searchParams = new URLSearchParams();
-
- const cloudPlanStore = useCloudPlanStore();
-
- if (deploymentType === 'cloud' && hasPermission(['instanceOwner'])) {
- const adminPanelHost = new URL(window.location.href).host.split('.').slice(1).join('.');
- const { code } = await cloudPlanStore.getAutoLoginCode();
- linkUrl = `https://${adminPanelHost}/login`;
- searchParams.set('code', code);
- searchParams.set('returnPath', '/account/change-plan');
- } else {
- linkUrl = N8N_PRICING_PAGE_URL;
- }
-
- if (utm_campaign) {
- searchParams.set('utm_campaign', utm_campaign);
- }
-
- if (source) {
- searchParams.set('source', source);
- }
- return `${linkUrl}?${searchParams.toString()}`;
-};
diff --git a/packages/editor-ui/src/stores/users.store.ts b/packages/editor-ui/src/stores/users.store.ts
index 4f17aa3f76765..4ba79024a27f5 100644
--- a/packages/editor-ui/src/stores/users.store.ts
+++ b/packages/editor-ui/src/stores/users.store.ts
@@ -18,7 +18,6 @@ import { getPersonalizedNodeTypes } from '@/utils/userUtils';
import { defineStore } from 'pinia';
import { useRootStore } from '@/stores/root.store';
import { usePostHog } from './posthog.store';
-import { useSettingsStore } from './settings.store';
import { useUIStore } from './ui.store';
import { useCloudPlanStore } from './cloudPlan.store';
import * as mfaApi from '@/api/mfa';
@@ -29,6 +28,7 @@ import * as invitationsApi from '@/api/invitation';
import { useNpsSurveyStore } from './npsSurvey.store';
import { computed, ref } from 'vue';
import { useTelemetry } from '@/composables/useTelemetry';
+import { useSettingsStore } from '@/stores/settings.store';
const _isPendingUser = (user: IUserResponse | null) => !!user?.isPending;
const _isInstanceOwner = (user: IUserResponse | null) => user?.role === ROLE.Owner;
@@ -49,6 +49,7 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
const rootStore = useRootStore();
const settingsStore = useSettingsStore();
const cloudPlanStore = useCloudPlanStore();
+
const telemetry = useTelemetry();
// Composables
diff --git a/packages/editor-ui/src/views/SettingsApiView.vue b/packages/editor-ui/src/views/SettingsApiView.vue
index 2bb67d04400ac..c8fc8bef4e841 100644
--- a/packages/editor-ui/src/views/SettingsApiView.vue
+++ b/packages/editor-ui/src/views/SettingsApiView.vue
@@ -13,6 +13,7 @@ import { useUIStore } from '@/stores/ui.store';
import { useUsersStore } from '@/stores/users.store';
import { useCloudPlanStore } from '@/stores/cloudPlan.store';
import { DOCS_DOMAIN, MODAL_CONFIRM } from '@/constants';
+import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
export default defineComponent({
name: 'SettingsApiView',
@@ -24,6 +25,7 @@ export default defineComponent({
...useToast(),
...useMessage(),
...useUIStore(),
+ pageRedirectionHelper: usePageRedirectionHelper(),
documentTitle: useDocumentTitle(),
};
},
@@ -70,7 +72,7 @@ export default defineComponent({
},
methods: {
onUpgrade() {
- void this.uiStore.goToUpgrade('settings-n8n-api', 'upgrade-api', 'redirect');
+ void this.pageRedirectionHelper.goToUpgrade('settings-n8n-api', 'upgrade-api', 'redirect');
},
async showDeleteModal() {
const confirmed = await this.confirm(
diff --git a/packages/editor-ui/src/views/SettingsExternalSecrets.vue b/packages/editor-ui/src/views/SettingsExternalSecrets.vue
index 30aaebeae1b7e..0d05bf60a2287 100644
--- a/packages/editor-ui/src/views/SettingsExternalSecrets.vue
+++ b/packages/editor-ui/src/views/SettingsExternalSecrets.vue
@@ -1,5 +1,4 @@
diff --git a/packages/editor-ui/src/views/SettingsLdapView.vue b/packages/editor-ui/src/views/SettingsLdapView.vue
index 41227c854e122..c1e71cd857699 100644
--- a/packages/editor-ui/src/views/SettingsLdapView.vue
+++ b/packages/editor-ui/src/views/SettingsLdapView.vue
@@ -20,10 +20,10 @@ import { ElTable, ElTableColumn } from 'element-plus';
import type { Events } from 'v3-infinite-loading';
import InfiniteLoading from 'v3-infinite-loading';
import { useSettingsStore } from '@/stores/settings.store';
-import { useUIStore } from '@/stores/ui.store';
import { createFormEventBus } from 'n8n-design-system/utils';
import type { TableColumnCtx } from 'element-plus';
import { useI18n } from '@/composables/useI18n';
+import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
type TableRow = {
status: string;
@@ -67,9 +67,9 @@ const toast = useToast();
const i18n = useI18n();
const message = useMessage();
const documentTitle = useDocumentTitle();
+const pageRedirectionHelper = usePageRedirectionHelper();
const settingsStore = useSettingsStore();
-const uiStore = useUIStore();
const dataTable = ref([]);
const tableKey = ref(0);
@@ -89,7 +89,7 @@ const ldapConfigFormRef = ref<{ getValues: () => LDAPConfigForm }>();
const isLDAPFeatureEnabled = computed(() => settingsStore.settings.enterprise.ldap);
-const goToUpgrade = async () => await uiStore.goToUpgrade('ldap', 'upgrade-ldap');
+const goToUpgrade = async () => await pageRedirectionHelper.goToUpgrade('ldap', 'upgrade-ldap');
const cellClassStyle = ({ row, column }: CellClassStyleMethodParams): CSSProperties => {
if (column.property === 'status') {
diff --git a/packages/editor-ui/src/views/SettingsLogStreamingView.vue b/packages/editor-ui/src/views/SettingsLogStreamingView.vue
index fae75b2604c21..5cc79c009d442 100644
--- a/packages/editor-ui/src/views/SettingsLogStreamingView.vue
+++ b/packages/editor-ui/src/views/SettingsLogStreamingView.vue
@@ -14,6 +14,7 @@ import { deepCopy, defaultMessageEventBusDestinationOptions } from 'n8n-workflow
import EventDestinationCard from '@/components/SettingsLogStreaming/EventDestinationCard.ee.vue';
import { createEventBus } from 'n8n-design-system/utils';
import { useDocumentTitle } from '@/composables/useDocumentTitle';
+import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
export default defineComponent({
name: 'SettingsLogStreamingView',
@@ -28,6 +29,7 @@ export default defineComponent({
disableLicense: false,
allDestinations: [] as MessageEventBusDestinationOptions[],
documentTitle: useDocumentTitle(),
+ pageRedirectionHelper: usePageRedirectionHelper(),
};
},
async mounted() {
@@ -117,7 +119,7 @@ export default defineComponent({
this.$forceUpdate();
},
goToUpgrade() {
- void this.uiStore.goToUpgrade('log-streaming', 'upgrade-log-streaming');
+ void this.pageRedirectionHelper.goToUpgrade('log-streaming', 'upgrade-log-streaming');
},
storeHasItems(): boolean {
return this.logStreamingStore.items && Object.keys(this.logStreamingStore.items).length > 0;
diff --git a/packages/editor-ui/src/views/SettingsSourceControl.vue b/packages/editor-ui/src/views/SettingsSourceControl.vue
index 3928556104a8a..eb29042230f96 100644
--- a/packages/editor-ui/src/views/SettingsSourceControl.vue
+++ b/packages/editor-ui/src/views/SettingsSourceControl.vue
@@ -3,7 +3,6 @@ import { computed, reactive, ref, onMounted } from 'vue';
import type { Rule, RuleGroup } from 'n8n-design-system/types';
import { MODAL_CONFIRM } from '@/constants';
import { useSourceControlStore } from '@/stores/sourceControl.store';
-import { useUIStore } from '@/stores/ui.store';
import { useToast } from '@/composables/useToast';
import { useLoadingService } from '@/composables/useLoadingService';
import { useI18n } from '@/composables/useI18n';
@@ -12,10 +11,11 @@ import { useDocumentTitle } from '@/composables/useDocumentTitle';
import CopyInput from '@/components/CopyInput.vue';
import type { TupleToUnion } from '@/utils/typeHelpers';
import type { SshKeyTypes } from '@/Interface';
+import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
const locale = useI18n();
const sourceControlStore = useSourceControlStore();
-const uiStore = useUIStore();
+const pageRedirectionHelper = usePageRedirectionHelper();
const toast = useToast();
const message = useMessage();
const documentTitle = useDocumentTitle();
@@ -102,7 +102,7 @@ const onSelect = async (b: string) => {
};
const goToUpgrade = () => {
- void uiStore.goToUpgrade('source-control', 'upgrade-source-control');
+ void pageRedirectionHelper.goToUpgrade('source-control', 'upgrade-source-control');
};
const initialize = async () => {
diff --git a/packages/editor-ui/src/views/SettingsSso.test.ts b/packages/editor-ui/src/views/SettingsSso.test.ts
index 793f28bda267a..0306bde35f210 100644
--- a/packages/editor-ui/src/views/SettingsSso.test.ts
+++ b/packages/editor-ui/src/views/SettingsSso.test.ts
@@ -2,10 +2,10 @@ import { createTestingPinia } from '@pinia/testing';
import { createComponentRenderer } from '@/__tests__/render';
import SettingsSso from './SettingsSso.vue';
import { useSSOStore } from '@/stores/sso.store';
-import { useUIStore } from '@/stores/ui.store';
import { within, waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { mockedStore } from '@/__tests__/utils';
+import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
const renderView = createComponentRenderer(SettingsSso);
@@ -38,6 +38,15 @@ vi.mock('@/composables/useMessage', () => ({
}),
}));
+vi.mock('@/composables/usePageRedirectionHelper', () => {
+ const goToUpgrade = vi.fn();
+ return {
+ usePageRedirectionHelper: () => ({
+ goToUpgrade,
+ }),
+ };
+});
+
describe('SettingsSso View', () => {
beforeEach(() => {
telemetryTrack.mockReset();
@@ -50,7 +59,7 @@ describe('SettingsSso View', () => {
const ssoStore = mockedStore(useSSOStore);
ssoStore.isEnterpriseSamlEnabled = false;
- const uiStore = useUIStore();
+ const pageRedirectionHelper = usePageRedirectionHelper();
const { getByTestId } = renderView({ pinia });
@@ -58,7 +67,7 @@ describe('SettingsSso View', () => {
expect(actionBox).toBeInTheDocument();
await userEvent.click(await within(actionBox).findByText('See plans'));
- expect(uiStore.goToUpgrade).toHaveBeenCalledWith('sso', 'upgrade-sso');
+ expect(pageRedirectionHelper.goToUpgrade).toHaveBeenCalledWith('sso', 'upgrade-sso');
});
it('should show user SSO config', async () => {
diff --git a/packages/editor-ui/src/views/SettingsSso.vue b/packages/editor-ui/src/views/SettingsSso.vue
index a9043c41aef35..64107ccba4286 100644
--- a/packages/editor-ui/src/views/SettingsSso.vue
+++ b/packages/editor-ui/src/views/SettingsSso.vue
@@ -1,7 +1,6 @@
diff --git a/packages/editor-ui/src/views/WorkflowHistory.vue b/packages/editor-ui/src/views/WorkflowHistory.vue
index 567cae9a41cd6..792f6b9ef51e3 100644
--- a/packages/editor-ui/src/views/WorkflowHistory.vue
+++ b/packages/editor-ui/src/views/WorkflowHistory.vue
@@ -20,6 +20,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { telemetry } from '@/plugins/telemetry';
import { useRootStore } from '@/stores/root.store';
import { getResourcePermissions } from '@/permissions';
+import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
type WorkflowHistoryActionRecord = {
[K in Uppercase]: Lowercase;
@@ -46,6 +47,8 @@ const route = useRoute();
const router = useRouter();
const i18n = useI18n();
const toast = useToast();
+const pageRedirectionHelper = usePageRedirectionHelper();
+
const workflowHistoryStore = useWorkflowHistoryStore();
const uiStore = useUIStore();
const workflowsStore = useWorkflowsStore();
@@ -296,7 +299,7 @@ const onPreview = async ({ event, id }: { event: MouseEvent; id: WorkflowVersion
};
const onUpgrade = () => {
- void uiStore.goToUpgrade('workflow-history', 'upgrade-workflow-history');
+ void pageRedirectionHelper.goToUpgrade('workflow-history', 'upgrade-workflow-history');
};
watchEffect(async () => {