mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
fix(editor): Move versions check to init function and refactor store (no-changelog) (#8067)
This commit is contained in:
parent
faadfd6d4a
commit
fcff34c401
32
cypress/composables/versions.ts
Normal file
32
cypress/composables/versions.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Getters
|
||||
*/
|
||||
|
||||
export function getVersionUpdatesPanelOpenButton() {
|
||||
return cy.getByTestId('version-updates-panel-button');
|
||||
}
|
||||
|
||||
export function getVersionUpdatesPanel() {
|
||||
return cy.getByTestId('version-updates-panel');
|
||||
}
|
||||
|
||||
export function getVersionUpdatesPanelCloseButton() {
|
||||
return getVersionUpdatesPanel().get('.el-drawer__close-btn').first();
|
||||
}
|
||||
|
||||
export function getVersionCard() {
|
||||
return cy.getByTestId('version-card');
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions
|
||||
*/
|
||||
|
||||
export function openVersionUpdatesPanel() {
|
||||
getVersionUpdatesPanelOpenButton().click();
|
||||
getVersionUpdatesPanel().should('be.visible');
|
||||
}
|
||||
|
||||
export function closeVersionUpdatesPanel() {
|
||||
getVersionUpdatesPanelCloseButton().click();
|
||||
}
|
66
cypress/e2e/36-versions.cy.ts
Normal file
66
cypress/e2e/36-versions.cy.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { INSTANCE_OWNER } from '../constants';
|
||||
import { WorkflowsPage } from '../pages/workflows';
|
||||
import {
|
||||
closeVersionUpdatesPanel,
|
||||
getVersionCard,
|
||||
getVersionUpdatesPanelOpenButton,
|
||||
openVersionUpdatesPanel,
|
||||
} from '../composables/versions';
|
||||
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
|
||||
describe('Versions', () => {
|
||||
it('should open updates panel', () => {
|
||||
cy.intercept('GET', '/rest/settings', (req) => {
|
||||
req.continue((res) => {
|
||||
if (res.body.hasOwnProperty('data')) {
|
||||
res.body.data = {
|
||||
...res.body.data,
|
||||
releaseChannel: 'stable',
|
||||
versionCli: '1.0.0',
|
||||
versionNotifications: {
|
||||
enabled: true,
|
||||
endpoint: 'https://api.n8n.io/api/versions/',
|
||||
infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html',
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
}).as('settings');
|
||||
|
||||
cy.intercept('GET', 'https://api.n8n.io/api/versions/1.0.0', [
|
||||
{
|
||||
name: '1.3.1',
|
||||
createdAt: '2023-08-18T11:53:12.857Z',
|
||||
hasSecurityIssue: null,
|
||||
hasSecurityFix: null,
|
||||
securityIssueFixVersion: null,
|
||||
hasBreakingChange: null,
|
||||
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n131',
|
||||
nodes: [],
|
||||
description: 'Includes <strong>bug fixes</strong>',
|
||||
},
|
||||
{
|
||||
name: '1.0.5',
|
||||
createdAt: '2023-07-24T10:54:56.097Z',
|
||||
hasSecurityIssue: false,
|
||||
hasSecurityFix: null,
|
||||
securityIssueFixVersion: null,
|
||||
hasBreakingChange: true,
|
||||
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n104',
|
||||
nodes: [],
|
||||
description: 'Includes <strong>core functionality</strong> and <strong>bug fixes</strong>',
|
||||
},
|
||||
]);
|
||||
|
||||
cy.signin(INSTANCE_OWNER);
|
||||
|
||||
cy.visit(workflowsPage.url);
|
||||
cy.wait('@settings');
|
||||
|
||||
getVersionUpdatesPanelOpenButton().should('contain', '2 updates');
|
||||
openVersionUpdatesPanel();
|
||||
getVersionCard().should('have.length', 2);
|
||||
closeVersionUpdatesPanel();
|
||||
});
|
||||
});
|
|
@ -35,7 +35,6 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { newVersions } from '@/mixins/newVersions';
|
||||
|
||||
import BannerStack from '@/components/banners/BannerStack.vue';
|
||||
import Modals from '@/components/Modals.vue';
|
||||
|
@ -69,15 +68,13 @@ export default defineComponent({
|
|||
Telemetry,
|
||||
Modals,
|
||||
},
|
||||
mixins: [newVersions, userHelpers],
|
||||
setup(props) {
|
||||
mixins: [userHelpers],
|
||||
setup() {
|
||||
return {
|
||||
...useGlobalLinkActions(),
|
||||
...useHistoryHelper(useRoute()),
|
||||
...useToast(),
|
||||
externalHooks: useExternalHooks(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
...newVersions.setup?.(props),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -115,7 +112,6 @@ export default defineComponent({
|
|||
async mounted() {
|
||||
this.logHiringBanner();
|
||||
|
||||
void this.checkForNewVersions();
|
||||
void initializeAuthenticatedFeatures();
|
||||
|
||||
void useExternalHooks().run('app.mount');
|
||||
|
|
|
@ -3,14 +3,14 @@ import { useCloudPlanStore } from '@/stores/cloudPlan.store';
|
|||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { initializeAuthenticatedFeatures } from '@/init';
|
||||
import type { SpyInstance } from 'vitest';
|
||||
import { initializeAuthenticatedFeatures, initializeCore } from '@/init';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useVersionsStore } from '@/stores/versions.store';
|
||||
|
||||
vi.mock('@/stores/users.store', () => ({
|
||||
useUsersStore: vi.fn(),
|
||||
useUsersStore: vi.fn().mockReturnValue({ initialize: vi.fn() }),
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/n8nRoot.store', () => ({
|
||||
|
@ -18,22 +18,48 @@ vi.mock('@/stores/n8nRoot.store', () => ({
|
|||
}));
|
||||
|
||||
describe('Init', () => {
|
||||
describe('Authenticated Features', () => {
|
||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
let cloudPlanStore: ReturnType<typeof useCloudPlanStore>;
|
||||
let sourceControlStore: ReturnType<typeof useSourceControlStore>;
|
||||
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
|
||||
let cloudStoreSpy: SpyInstance<[], Promise<void>>;
|
||||
let templatesTestSpy: SpyInstance<[], Promise<void>>;
|
||||
let sourceControlSpy: SpyInstance<[], Promise<void>>;
|
||||
let nodeTranslationSpy: SpyInstance<[], Promise<void>>;
|
||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
||||
let cloudPlanStore: ReturnType<typeof useCloudPlanStore>;
|
||||
let sourceControlStore: ReturnType<typeof useSourceControlStore>;
|
||||
let usersStore: ReturnType<typeof useUsersStore>;
|
||||
let nodeTypesStore: ReturnType<typeof useNodeTypesStore>;
|
||||
let versionsStore: ReturnType<typeof useVersionsStore>;
|
||||
|
||||
beforeAll(() => {
|
||||
setActivePinia(createTestingPinia());
|
||||
settingsStore = useSettingsStore();
|
||||
cloudPlanStore = useCloudPlanStore();
|
||||
sourceControlStore = useSourceControlStore();
|
||||
nodeTypesStore = useNodeTypesStore();
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia());
|
||||
settingsStore = useSettingsStore();
|
||||
cloudPlanStore = useCloudPlanStore();
|
||||
sourceControlStore = useSourceControlStore();
|
||||
nodeTypesStore = useNodeTypesStore();
|
||||
usersStore = useUsersStore();
|
||||
versionsStore = useVersionsStore();
|
||||
versionsStore = useVersionsStore();
|
||||
});
|
||||
|
||||
describe('initializeCore()', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should initialize core features only once', async () => {
|
||||
const usersStoreSpy = vi.spyOn(usersStore, 'initialize');
|
||||
const settingsStoreSpy = vi.spyOn(settingsStore, 'initialize');
|
||||
const versionsSpy = vi.spyOn(versionsStore, 'checkForNewVersions');
|
||||
|
||||
await initializeCore();
|
||||
|
||||
expect(settingsStoreSpy).toHaveBeenCalled();
|
||||
expect(usersStoreSpy).toHaveBeenCalled();
|
||||
expect(versionsSpy).toHaveBeenCalled();
|
||||
|
||||
await initializeCore();
|
||||
|
||||
expect(settingsStoreSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeAuthenticatedFeatures()', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(settingsStore, 'isCloudDeployment', 'get').mockReturnValue(true);
|
||||
vi.spyOn(settingsStore, 'isTemplatesEnabled', 'get').mockReturnValue(true);
|
||||
vi.spyOn(sourceControlStore, 'isEnterpriseSourceControlEnabled', 'get').mockReturnValue(true);
|
||||
|
@ -43,10 +69,6 @@ describe('Init', () => {
|
|||
vi.mock('@/hooks/register', () => ({
|
||||
initializeCloudHooks: vi.fn(),
|
||||
}));
|
||||
cloudStoreSpy = vi.spyOn(cloudPlanStore, 'initialize');
|
||||
templatesTestSpy = vi.spyOn(settingsStore, 'testTemplatesEndpoint');
|
||||
sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences');
|
||||
nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -54,24 +76,40 @@ describe('Init', () => {
|
|||
});
|
||||
|
||||
it('should not init authenticated features if user is not logged in', async () => {
|
||||
const cloudStoreSpy = vi.spyOn(cloudPlanStore, 'initialize');
|
||||
const templatesTestSpy = vi.spyOn(settingsStore, 'testTemplatesEndpoint');
|
||||
const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences');
|
||||
const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders');
|
||||
vi.mocked(useUsersStore).mockReturnValue({ currentUser: null } as ReturnType<
|
||||
typeof useUsersStore
|
||||
>);
|
||||
|
||||
await initializeAuthenticatedFeatures();
|
||||
expect(cloudStoreSpy).not.toHaveBeenCalled();
|
||||
expect(templatesTestSpy).not.toHaveBeenCalled();
|
||||
expect(sourceControlSpy).not.toHaveBeenCalled();
|
||||
expect(nodeTranslationSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should init authenticated features if user is not logged in', async () => {
|
||||
|
||||
it('should init authenticated features only once if user is logged in', async () => {
|
||||
const cloudStoreSpy = vi.spyOn(cloudPlanStore, 'initialize');
|
||||
const templatesTestSpy = vi.spyOn(settingsStore, 'testTemplatesEndpoint');
|
||||
const sourceControlSpy = vi.spyOn(sourceControlStore, 'getPreferences');
|
||||
const nodeTranslationSpy = vi.spyOn(nodeTypesStore, 'getNodeTranslationHeaders');
|
||||
vi.mocked(useUsersStore).mockReturnValue({ currentUser: { id: '123' } } as ReturnType<
|
||||
typeof useUsersStore
|
||||
>);
|
||||
|
||||
await initializeAuthenticatedFeatures();
|
||||
|
||||
expect(cloudStoreSpy).toHaveBeenCalled();
|
||||
expect(templatesTestSpy).toHaveBeenCalled();
|
||||
expect(sourceControlSpy).toHaveBeenCalled();
|
||||
expect(nodeTranslationSpy).toHaveBeenCalled();
|
||||
|
||||
await initializeAuthenticatedFeatures();
|
||||
|
||||
expect(cloudStoreSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -29,7 +29,12 @@
|
|||
/></template>
|
||||
<template #menuSuffix>
|
||||
<div>
|
||||
<div v-if="hasVersionUpdates" :class="$style.updates" @click="openUpdatesPanel">
|
||||
<div
|
||||
v-if="hasVersionUpdates"
|
||||
data-test-id="version-updates-panel-button"
|
||||
:class="$style.updates"
|
||||
@click="openUpdatesPanel"
|
||||
>
|
||||
<div :class="$style.giftContainer">
|
||||
<GiftNotificationIcon />
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<template>
|
||||
<ModalDrawer :name="VERSIONS_MODAL_KEY" direction="ltr" width="520px">
|
||||
<ModalDrawer
|
||||
:name="VERSIONS_MODAL_KEY"
|
||||
direction="ltr"
|
||||
width="520px"
|
||||
data-test-id="version-updates-panel"
|
||||
>
|
||||
<template #header>
|
||||
<span :class="$style.title">
|
||||
{{ $locale.baseText('updatesPanel.weVeBeenBusy') }}
|
||||
|
@ -31,7 +36,7 @@
|
|||
</p>
|
||||
|
||||
<n8n-link v-if="infoUrl" :to="infoUrl" :bold="true">
|
||||
<font-awesome-icon icon="info-circle"></font-awesome-icon>
|
||||
<font-awesome-icon icon="info-circle" class="mr-2xs" />
|
||||
<span>
|
||||
{{ $locale.baseText('updatesPanel.howToUpdateYourN8nVersion') }}
|
||||
</span>
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
<template>
|
||||
<a v-if="version" :href="version.documentationUrl" target="_blank" :class="$style.card">
|
||||
<a
|
||||
v-if="version"
|
||||
:href="version.documentationUrl"
|
||||
target="_blank"
|
||||
:class="$style.card"
|
||||
data-test-id="version-card"
|
||||
>
|
||||
<div :class="$style.header">
|
||||
<div>
|
||||
<div :class="$style.name">
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useSettingsStore } from '@/stores/settings.store';
|
|||
import { useSourceControlStore } from '@/stores/sourceControl.store';
|
||||
import { useUsersStore } from '@/stores/users.store';
|
||||
import { initializeCloudHooks } from '@/hooks/register';
|
||||
import { useVersionsStore } from '@/stores/versions.store';
|
||||
|
||||
let coreInitialized = false;
|
||||
let authenticatedFeaturesInitialized = false;
|
||||
|
@ -20,10 +21,13 @@ export async function initializeCore() {
|
|||
|
||||
const settingsStore = useSettingsStore();
|
||||
const usersStore = useUsersStore();
|
||||
const versionsStore = useVersionsStore();
|
||||
|
||||
await settingsStore.initialize();
|
||||
await usersStore.initialize();
|
||||
|
||||
void versionsStore.checkForNewVersions();
|
||||
|
||||
if (settingsStore.isCloudDeployment) {
|
||||
try {
|
||||
await initializeCloudHooks();
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { VERSIONS_MODAL_KEY } from '@/constants';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useVersionsStore } from '@/stores/versions.store';
|
||||
|
||||
export const newVersions = defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useUIStore, useVersionsStore),
|
||||
},
|
||||
methods: {
|
||||
async checkForNewVersions() {
|
||||
const enabled = this.versionsStore.areNotificationsEnabled;
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.versionsStore.fetchVersions();
|
||||
|
||||
const currentVersion = this.versionsStore.currentVersion;
|
||||
const nextVersions = this.versionsStore.nextVersions;
|
||||
if (currentVersion && currentVersion.hasSecurityIssue && nextVersions.length) {
|
||||
const fixVersion = currentVersion.securityIssueFixVersion;
|
||||
let message = 'Please update to latest version.';
|
||||
if (fixVersion) {
|
||||
message = `Please update to version ${fixVersion} or higher.`;
|
||||
}
|
||||
|
||||
message = `${message} <a class="primary-color">More info</a>`;
|
||||
this.showToast({
|
||||
title: 'Critical update available',
|
||||
message,
|
||||
onClick: () => {
|
||||
this.uiStore.openModal(VERSIONS_MODAL_KEY);
|
||||
},
|
||||
closeOnClick: true,
|
||||
customClass: 'clickable',
|
||||
type: 'warning',
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,8 +1,10 @@
|
|||
import { getNextVersions } from '@/api/versions';
|
||||
import { STORES } from '@/constants';
|
||||
import { STORES, VERSIONS_MODAL_KEY } from '@/constants';
|
||||
import type { IVersion, IVersionNotificationSettings, IVersionsState } from '@/Interface';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useRootStore } from './n8nRoot.store';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
|
||||
export const useVersionsStore = defineStore(STORES.VERSIONS, {
|
||||
state: (): IVersionsState => ({
|
||||
|
@ -45,5 +47,40 @@ export const useVersionsStore = defineStore(STORES.VERSIONS, {
|
|||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
async checkForNewVersions() {
|
||||
const enabled = this.areNotificationsEnabled;
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { showToast } = useToast();
|
||||
const uiStore = useUIStore();
|
||||
|
||||
await this.fetchVersions();
|
||||
|
||||
const currentVersion = this.currentVersion;
|
||||
const nextVersions = this.nextVersions;
|
||||
|
||||
if (currentVersion && currentVersion.hasSecurityIssue && nextVersions.length) {
|
||||
const fixVersion = currentVersion.securityIssueFixVersion;
|
||||
let message = 'Please update to latest version.';
|
||||
if (fixVersion) {
|
||||
message = `Please update to version ${fixVersion} or higher.`;
|
||||
}
|
||||
|
||||
message = `${message} <a class="primary-color">More info</a>`;
|
||||
showToast({
|
||||
title: 'Critical update available',
|
||||
message,
|
||||
onClick: () => {
|
||||
uiStore.openModal(VERSIONS_MODAL_KEY);
|
||||
},
|
||||
closeOnClick: true,
|
||||
customClass: 'clickable',
|
||||
type: 'warning',
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue