mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
fix(editor): Don't show update notification for unverified updates (#18910)
Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
@@ -26,6 +26,12 @@
|
|||||||
"test:postgres": "N8N_LOG_LEVEL=silent DB_TYPE=postgresdb DB_POSTGRESDB_SCHEMA=alt_schema DB_TABLE_PREFIX=test_ jest --no-coverage",
|
"test:postgres": "N8N_LOG_LEVEL=silent DB_TYPE=postgresdb DB_POSTGRESDB_SCHEMA=alt_schema DB_TABLE_PREFIX=test_ jest --no-coverage",
|
||||||
"test:mariadb": "N8N_LOG_LEVEL=silent DB_TYPE=mariadb DB_TABLE_PREFIX=test_ jest --no-coverage",
|
"test:mariadb": "N8N_LOG_LEVEL=silent DB_TYPE=mariadb DB_TABLE_PREFIX=test_ jest --no-coverage",
|
||||||
"test:mysql": "N8N_LOG_LEVEL=silent DB_TYPE=mysqldb DB_TABLE_PREFIX=test_ jest --no-coverage",
|
"test:mysql": "N8N_LOG_LEVEL=silent DB_TYPE=mysqldb DB_TABLE_PREFIX=test_ jest --no-coverage",
|
||||||
|
"test:win": "pnpm test:sqlite:win",
|
||||||
|
"test:dev:win": "set N8N_LOG_LEVEL=silent&& set DB_TYPE=sqlite&& jest --watch",
|
||||||
|
"test:sqlite:win": "set N8N_LOG_LEVEL=silent&& set DB_TYPE=sqlite&& jest",
|
||||||
|
"test:postgres:win": "set N8N_LOG_LEVEL=silent&& set DB_TYPE=postgresdb&& set DB_POSTGRESDB_SCHEMA=alt_schema&& set DB_TABLE_PREFIX=test_&& jest --no-coverage",
|
||||||
|
"test:mariadb:win": "set N8N_LOG_LEVEL=silent&& set DB_TYPE=mariadb&& set DB_TABLE_PREFIX=test_&& jest --no-coverage",
|
||||||
|
"test:mysql:win": "set N8N_LOG_LEVEL=silent&& set DB_TYPE=mysqldb&& set DB_TABLE_PREFIX=test_&& jest --no-coverage",
|
||||||
"watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\""
|
"watch": "tsc-watch -p tsconfig.build.json --onCompilationComplete \"tsc-alias -p tsconfig.build.json\""
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Logger } from '@n8n/backend-common';
|
import type { Logger } from '@n8n/backend-common';
|
||||||
import { randomName, mockInstance } from '@n8n/backend-test-utils';
|
import { mockInstance, randomName } from '@n8n/backend-test-utils';
|
||||||
import { LICENSE_FEATURES } from '@n8n/constants';
|
import { LICENSE_FEATURES } from '@n8n/constants';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { mocked } from 'jest-mock';
|
import { mocked } from 'jest-mock';
|
||||||
@@ -7,8 +7,8 @@ import { mock } from 'jest-mock-extended';
|
|||||||
import type { InstanceSettings, PackageDirectoryLoader } from 'n8n-core';
|
import type { InstanceSettings, PackageDirectoryLoader } from 'n8n-core';
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||||
import { exec } from 'node:child_process';
|
import { exec } from 'node:child_process';
|
||||||
import { mkdir, readFile, writeFile, rm, access, constants } from 'node:fs/promises';
|
import { access, constants, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import path, { join } from 'node:path';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NODE_PACKAGE_PREFIX,
|
NODE_PACKAGE_PREFIX,
|
||||||
@@ -54,7 +54,7 @@ describe('CommunityPackagesService', () => {
|
|||||||
const installedNodesRepository = mockInstance(InstalledNodesRepository);
|
const installedNodesRepository = mockInstance(InstalledNodesRepository);
|
||||||
const installedPackageRepository = mockInstance(InstalledPackagesRepository);
|
const installedPackageRepository = mockInstance(InstalledPackagesRepository);
|
||||||
|
|
||||||
const nodesDownloadDir = '/tmp/n8n-jest-global-downloads';
|
const nodesDownloadDir = path.join('tmp', 'n8n-jest-global-downloads');
|
||||||
const instanceSettings = mock<InstanceSettings>({ nodesDownloadDir });
|
const instanceSettings = mock<InstanceSettings>({ nodesDownloadDir });
|
||||||
|
|
||||||
const logger = mock<Logger>();
|
const logger = mock<Logger>();
|
||||||
@@ -441,7 +441,10 @@ describe('CommunityPackagesService', () => {
|
|||||||
// ASSERT:
|
// ASSERT:
|
||||||
expect(rm).toHaveBeenCalledTimes(2);
|
expect(rm).toHaveBeenCalledTimes(2);
|
||||||
expect(rm).toHaveBeenNthCalledWith(1, testBlockPackageDir, { recursive: true, force: true });
|
expect(rm).toHaveBeenNthCalledWith(1, testBlockPackageDir, { recursive: true, force: true });
|
||||||
expect(rm).toHaveBeenNthCalledWith(2, `${nodesDownloadDir}/n8n-nodes-test-latest.tgz`);
|
expect(rm).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
path.join(nodesDownloadDir, 'n8n-nodes-test-latest.tgz'),
|
||||||
|
);
|
||||||
|
|
||||||
expect(exec).toHaveBeenCalledTimes(3);
|
expect(exec).toHaveBeenCalledTimes(3);
|
||||||
expect(exec).toHaveBeenNthCalledWith(
|
expect(exec).toHaveBeenNthCalledWith(
|
||||||
@@ -672,7 +675,7 @@ describe('CommunityPackagesService', () => {
|
|||||||
await communityPackagesService.updatePackageJsonDependency('test-package', '1.0.0');
|
await communityPackagesService.updatePackageJsonDependency('test-package', '1.0.0');
|
||||||
|
|
||||||
expect(writeFile).toHaveBeenCalledWith(
|
expect(writeFile).toHaveBeenCalledWith(
|
||||||
`${nodesDownloadDir}/package.json`,
|
path.join(nodesDownloadDir, 'package.json'),
|
||||||
JSON.stringify({ dependencies: { 'test-package': '1.0.0' } }, null, 2),
|
JSON.stringify({ dependencies: { 'test-package': '1.0.0' } }, null, 2),
|
||||||
'utf-8',
|
'utf-8',
|
||||||
);
|
);
|
||||||
@@ -682,7 +685,7 @@ describe('CommunityPackagesService', () => {
|
|||||||
await communityPackagesService.updatePackageJsonDependency('test-package', '1.0.0');
|
await communityPackagesService.updatePackageJsonDependency('test-package', '1.0.0');
|
||||||
|
|
||||||
expect(writeFile).toHaveBeenCalledWith(
|
expect(writeFile).toHaveBeenCalledWith(
|
||||||
`${nodesDownloadDir}/package.json`,
|
path.join(nodesDownloadDir, 'package.json'),
|
||||||
JSON.stringify({ dependencies: { 'test-package': '1.0.0' } }, null, 2),
|
JSON.stringify({ dependencies: { 'test-package': '1.0.0' } }, null, 2),
|
||||||
'utf-8',
|
'utf-8',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,18 +5,16 @@ import { setActivePinia } from 'pinia';
|
|||||||
import CommunityNodeFooter from './CommunityNodeFooter.vue';
|
import CommunityNodeFooter from './CommunityNodeFooter.vue';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
import { type ExtendedPublicInstalledPackage, fetchInstalledPackageInfo } from './utils';
|
||||||
|
|
||||||
const getInstalledPackage = vi.fn();
|
vi.mock('./utils', () => ({
|
||||||
|
fetchInstalledPackageInfo: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockedFetchInstalledPackageInfo = vi.mocked(fetchInstalledPackageInfo);
|
||||||
|
|
||||||
const push = vi.fn();
|
const push = vi.fn();
|
||||||
|
|
||||||
const communityNodesStore: {
|
|
||||||
getInstalledPackage: (packageName: string) => Promise<PublicInstalledPackage>;
|
|
||||||
} = {
|
|
||||||
getInstalledPackage,
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.mock('vue-router', async (importOriginal) => {
|
vi.mock('vue-router', async (importOriginal) => {
|
||||||
const actual = await importOriginal();
|
const actual = await importOriginal();
|
||||||
return {
|
return {
|
||||||
@@ -27,10 +25,6 @@ vi.mock('vue-router', async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('@/stores/communityNodes.store', () => ({
|
|
||||||
useCommunityNodesStore: vi.fn(() => communityNodesStore),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('CommunityNodeInfo - links & bugs URL', () => {
|
describe('CommunityNodeInfo - links & bugs URL', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setActivePinia(createTestingPinia());
|
setActivePinia(createTestingPinia());
|
||||||
@@ -70,25 +64,27 @@ describe('CommunityNodeInfo - links & bugs URL', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('displays "Legacy" when updateAvailable', async () => {
|
it('displays "Legacy" when updateAvailable', async () => {
|
||||||
getInstalledPackage.mockResolvedValue({
|
mockedFetchInstalledPackageInfo.mockResolvedValue({
|
||||||
installedVersion: '1.0.0',
|
installedVersion: '1.0.0',
|
||||||
updateAvailable: '1.0.1',
|
updateAvailable: '1.0.1',
|
||||||
} as PublicInstalledPackage);
|
unverifiedUpdate: false,
|
||||||
|
} as ExtendedPublicInstalledPackage);
|
||||||
const { getByText } = createComponentRenderer(CommunityNodeFooter)({
|
const { getByText } = createComponentRenderer(CommunityNodeFooter)({
|
||||||
props: {
|
props: {
|
||||||
packageName: 'n8n-nodes-test',
|
packageName: 'n8n-nodes-test',
|
||||||
showManage: false,
|
showManage: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await waitFor(() => expect(getInstalledPackage).toHaveBeenCalled());
|
await waitFor(() => expect(mockedFetchInstalledPackageInfo).toHaveBeenCalled());
|
||||||
|
|
||||||
expect(getByText('Package version 1.0.0 (Legacy)')).toBeInTheDocument();
|
expect(getByText('Package version 1.0.0 (Legacy)')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays "Latest" when not updateAvailable', async () => {
|
it('displays "Latest" when not updateAvailable', async () => {
|
||||||
getInstalledPackage.mockResolvedValue({
|
mockedFetchInstalledPackageInfo.mockResolvedValue({
|
||||||
installedVersion: '1.0.0',
|
installedVersion: '1.0.0',
|
||||||
} as PublicInstalledPackage);
|
unverifiedUpdate: false,
|
||||||
|
} as ExtendedPublicInstalledPackage);
|
||||||
const { getByText } = createComponentRenderer(CommunityNodeFooter)({
|
const { getByText } = createComponentRenderer(CommunityNodeFooter)({
|
||||||
props: {
|
props: {
|
||||||
packageName: 'n8n-nodes-test',
|
packageName: 'n8n-nodes-test',
|
||||||
@@ -96,7 +92,24 @@ describe('CommunityNodeInfo - links & bugs URL', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => expect(getInstalledPackage).toHaveBeenCalled());
|
await waitFor(() => expect(mockedFetchInstalledPackageInfo).toHaveBeenCalled());
|
||||||
|
|
||||||
|
expect(getByText('Package version 1.0.0 (Latest)')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays "Latest" when only unverified update is available', async () => {
|
||||||
|
mockedFetchInstalledPackageInfo.mockResolvedValue({
|
||||||
|
installedVersion: '1.0.0',
|
||||||
|
unverifiedUpdate: true,
|
||||||
|
} as ExtendedPublicInstalledPackage);
|
||||||
|
const { getByText } = createComponentRenderer(CommunityNodeFooter)({
|
||||||
|
props: {
|
||||||
|
packageName: 'n8n-nodes-test',
|
||||||
|
showManage: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(mockedFetchInstalledPackageInfo).toHaveBeenCalled());
|
||||||
|
|
||||||
expect(getByText('Package version 1.0.0 (Latest)')).toBeInTheDocument();
|
expect(getByText('Package version 1.0.0 (Latest)')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
|
import { captureException } from '@sentry/vue';
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { captureException } from '@sentry/vue';
|
|
||||||
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
|
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { N8nText, N8nLink } from '@n8n/design-system';
|
import { N8nLink, N8nText } from '@n8n/design-system';
|
||||||
|
import { fetchInstalledPackageInfo, type ExtendedPublicInstalledPackage } from './utils';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
packageName: string;
|
packageName: string;
|
||||||
@@ -17,7 +16,7 @@ const props = defineProps<Props>();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const bugsUrl = ref<string>(`https://registry.npmjs.org/${props.packageName}`);
|
const bugsUrl = ref<string>(`https://registry.npmjs.org/${props.packageName}`);
|
||||||
const installedPackage = ref<PublicInstalledPackage | undefined>(undefined);
|
const installedPackage = ref<ExtendedPublicInstalledPackage | undefined>(undefined);
|
||||||
|
|
||||||
async function openSettingsPage() {
|
async function openSettingsPage() {
|
||||||
await router.push({ name: VIEWS.COMMUNITY_NODES });
|
await router.push({ name: VIEWS.COMMUNITY_NODES });
|
||||||
@@ -52,7 +51,7 @@ async function getBugsUrl(packageName: string) {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (props.packageName) {
|
if (props.packageName) {
|
||||||
await getBugsUrl(props.packageName);
|
await getBugsUrl(props.packageName);
|
||||||
installedPackage.value = await useCommunityNodesStore().getInstalledPackage(props.packageName);
|
installedPackage.value = await fetchInstalledPackageInfo(props.packageName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -63,7 +62,9 @@ onMounted(async () => {
|
|||||||
<div :class="$style.container">
|
<div :class="$style.container">
|
||||||
<N8nText v-if="installedPackage" size="small" color="text-light" style="margin-right: auto">
|
<N8nText v-if="installedPackage" size="small" color="text-light" style="margin-right: auto">
|
||||||
Package version {{ installedPackage.installedVersion }} ({{
|
Package version {{ installedPackage.installedVersion }} ({{
|
||||||
installedPackage.updateAvailable ? 'Legacy' : 'Latest'
|
installedPackage.updateAvailable && !installedPackage.unverifiedUpdate
|
||||||
|
? 'Legacy'
|
||||||
|
: 'Latest'
|
||||||
}})
|
}})
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<template v-if="props.showManage">
|
<template v-if="props.showManage">
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { type TestingPinia, createTestingPinia } from '@pinia/testing';
|
import { type TestingPinia, createTestingPinia } from '@pinia/testing';
|
||||||
import { setActivePinia } from 'pinia';
|
|
||||||
import CommunityNodeInfo from './CommunityNodeInfo.vue';
|
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
|
||||||
import { waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
import type { CommunityNodeDetails } from '../composables/useViewStacks';
|
import type { CommunityNodeDetails } from '../composables/useViewStacks';
|
||||||
|
import CommunityNodeInfo from './CommunityNodeInfo.vue';
|
||||||
|
import { type ExtendedPublicInstalledPackage, fetchInstalledPackageInfo } from './utils';
|
||||||
|
|
||||||
|
vi.mock('./utils', () => ({
|
||||||
|
fetchInstalledPackageInfo: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockedFetchInstalledPackageInfo = vi.mocked(fetchInstalledPackageInfo);
|
||||||
|
|
||||||
const getCommunityNodeAttributes = vi.fn();
|
const getCommunityNodeAttributes = vi.fn();
|
||||||
const getInstalledPackage = vi.fn();
|
|
||||||
const communityNodesStore: {
|
|
||||||
getInstalledPackage: (packageName: string) => Promise<PublicInstalledPackage>;
|
|
||||||
} = {
|
|
||||||
getInstalledPackage,
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.mock('@/stores/nodeTypes.store', () => ({
|
vi.mock('@/stores/nodeTypes.store', () => ({
|
||||||
useNodeTypesStore: vi.fn(() => ({
|
useNodeTypesStore: vi.fn(() => ({
|
||||||
@@ -26,10 +26,6 @@ vi.mock('@/stores/users.store', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@/stores/communityNodes.store', () => ({
|
|
||||||
useCommunityNodesStore: vi.fn(() => communityNodesStore),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('../composables/useViewStacks', () => ({
|
vi.mock('../composables/useViewStacks', () => ({
|
||||||
useViewStacks: vi.fn(),
|
useViewStacks: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -98,10 +94,11 @@ describe('CommunityNodeInfo', () => {
|
|||||||
numberOfDownloads: 9999,
|
numberOfDownloads: 9999,
|
||||||
nodeVersions: [{ npmVersion: '1.0.0' }],
|
nodeVersions: [{ npmVersion: '1.0.0' }],
|
||||||
});
|
});
|
||||||
getInstalledPackage.mockResolvedValue({
|
mockedFetchInstalledPackageInfo.mockResolvedValue({
|
||||||
installedVersion: '1.0.0',
|
installedVersion: '1.0.0',
|
||||||
packageName: 'n8n-nodes-test',
|
packageName: 'n8n-nodes-test',
|
||||||
} as PublicInstalledPackage);
|
unverifiedUpdate: false,
|
||||||
|
} as ExtendedPublicInstalledPackage);
|
||||||
|
|
||||||
const wrapper = renderComponent({ pinia });
|
const wrapper = renderComponent({ pinia });
|
||||||
|
|
||||||
@@ -133,11 +130,12 @@ describe('CommunityNodeInfo', () => {
|
|||||||
numberOfDownloads: 9999,
|
numberOfDownloads: 9999,
|
||||||
nodeVersions: [{ npmVersion: '1.0.0' }, { npmVersion: '0.0.9' }],
|
nodeVersions: [{ npmVersion: '1.0.0' }, { npmVersion: '0.0.9' }],
|
||||||
});
|
});
|
||||||
getInstalledPackage.mockResolvedValue({
|
mockedFetchInstalledPackageInfo.mockResolvedValue({
|
||||||
installedVersion: '0.0.9',
|
installedVersion: '0.0.9',
|
||||||
packageName: 'n8n-nodes-test',
|
packageName: 'n8n-nodes-test',
|
||||||
updateAvailable: '1.0.1',
|
updateAvailable: '1.0.1',
|
||||||
} as PublicInstalledPackage);
|
unverifiedUpdate: false,
|
||||||
|
} as ExtendedPublicInstalledPackage);
|
||||||
|
|
||||||
const wrapper = renderComponent({ pinia });
|
const wrapper = renderComponent({ pinia });
|
||||||
|
|
||||||
@@ -154,6 +152,38 @@ describe('CommunityNodeInfo', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should NOT display update notice for unverified update', async () => {
|
||||||
|
const { useViewStacks } = await import('../composables/useViewStacks');
|
||||||
|
vi.mocked(useViewStacks).mockReturnValue({
|
||||||
|
activeViewStack: {
|
||||||
|
...defaultViewStack,
|
||||||
|
communityNodeDetails: {
|
||||||
|
...defaultViewStack.communityNodeDetails,
|
||||||
|
installed: true,
|
||||||
|
} as CommunityNodeDetails,
|
||||||
|
},
|
||||||
|
} as ReturnType<typeof useViewStacks>);
|
||||||
|
|
||||||
|
getCommunityNodeAttributes.mockResolvedValue({
|
||||||
|
npmVersion: '1.0.0',
|
||||||
|
authorName: 'contributor',
|
||||||
|
numberOfDownloads: 9999,
|
||||||
|
nodeVersions: [{ npmVersion: '1.0.0' }, { npmVersion: '0.0.9' }],
|
||||||
|
});
|
||||||
|
mockedFetchInstalledPackageInfo.mockResolvedValue({
|
||||||
|
installedVersion: '0.0.9',
|
||||||
|
packageName: 'n8n-nodes-test',
|
||||||
|
updateAvailable: '1.0.1',
|
||||||
|
unverifiedUpdate: true,
|
||||||
|
} as ExtendedPublicInstalledPackage);
|
||||||
|
|
||||||
|
const wrapper = renderComponent({ pinia });
|
||||||
|
|
||||||
|
await waitFor(() => expect(wrapper.queryByTestId('number-of-downloads')).toBeInTheDocument());
|
||||||
|
|
||||||
|
expect(wrapper.queryByTestId('update-available')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('should render correctly with fetched info', async () => {
|
it('should render correctly with fetched info', async () => {
|
||||||
const packageData = {
|
const packageData = {
|
||||||
maintainers: [{ name: 'testAuthor' }],
|
maintainers: [{ name: 'testAuthor' }],
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import { useViewStacks } from '../composables/useViewStacks';
|
|||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { i18n } from '@n8n/i18n';
|
import { i18n } from '@n8n/i18n';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
|
|
||||||
import { captureException } from '@sentry/vue';
|
import { captureException } from '@sentry/vue';
|
||||||
import { N8nText, N8nTooltip, N8nIcon } from '@n8n/design-system';
|
import { N8nText, N8nTooltip, N8nIcon } from '@n8n/design-system';
|
||||||
import ShieldIcon from 'virtual:icons/fa-solid/shield-alt';
|
import ShieldIcon from 'virtual:icons/fa-solid/shield-alt';
|
||||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
import { type ExtendedPublicInstalledPackage, fetchInstalledPackageInfo } from './utils';
|
||||||
|
|
||||||
const { activeViewStack } = useViewStacks();
|
const { activeViewStack } = useViewStacks();
|
||||||
|
|
||||||
@@ -22,9 +21,8 @@ const publisherName = ref<string | undefined>(undefined);
|
|||||||
const downloads = ref<string | null>(null);
|
const downloads = ref<string | null>(null);
|
||||||
const verified = ref(false);
|
const verified = ref(false);
|
||||||
const official = ref(false);
|
const official = ref(false);
|
||||||
const installedPackage = ref<PublicInstalledPackage | undefined>(undefined);
|
const installedPackage = ref<ExtendedPublicInstalledPackage | undefined>(undefined);
|
||||||
|
|
||||||
const communityNodesStore = useCommunityNodesStore();
|
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
|
||||||
const isOwner = computed(() => useUsersStore().isInstanceOwner);
|
const isOwner = computed(() => useUsersStore().isInstanceOwner);
|
||||||
@@ -46,9 +44,7 @@ async function fetchPackageInfo(packageName: string) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (communityNodeDetails?.installed) {
|
if (communityNodeDetails?.installed) {
|
||||||
installedPackage.value = await communityNodesStore.getInstalledPackage(
|
installedPackage.value = await fetchInstalledPackageInfo(communityNodeDetails.packageName);
|
||||||
communityNodeDetails.packageName,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (communityNodeAttributes) {
|
if (communityNodeAttributes) {
|
||||||
@@ -116,7 +112,7 @@ onMounted(async () => {
|
|||||||
{{ communityNodeDetails?.description }}
|
{{ communityNodeDetails?.description }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<CommunityNodeUpdateInfo
|
<CommunityNodeUpdateInfo
|
||||||
v-if="isOwner && installedPackage?.updateAvailable"
|
v-if="isOwner && installedPackage?.updateAvailable && !installedPackage.unverifiedUpdate"
|
||||||
data-test-id="update-available"
|
data-test-id="update-available"
|
||||||
/>
|
/>
|
||||||
<div v-else :class="$style.separator"></div>
|
<div v-else :class="$style.separator"></div>
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { fetchInstalledPackageInfo } from './utils';
|
||||||
|
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
|
||||||
|
import { type NodeTypesStore, useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||||
|
import type { CommunityNodeType } from '@n8n/api-types';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
|
||||||
|
vi.mock('@/stores/communityNodes.store', () => ({
|
||||||
|
useCommunityNodesStore: vi.fn(() => ({
|
||||||
|
getInstalledPackage: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@/stores/nodeTypes.store', () => ({
|
||||||
|
useNodeTypesStore: vi.fn(() => ({
|
||||||
|
communityNodeType: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
type CommunityNodesStore = ReturnType<typeof useCommunityNodesStore>;
|
||||||
|
|
||||||
|
const mockCommunityNodesStore = (mock: Partial<CommunityNodesStore>) => {
|
||||||
|
vi.mocked(useCommunityNodesStore).mockImplementation(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
getInstalledPackage: vi.fn(),
|
||||||
|
...mock,
|
||||||
|
}) as unknown as CommunityNodesStore,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockNodeTypesStore = (mock: Partial<NodeTypesStore>) => {
|
||||||
|
vi.mocked(useNodeTypesStore).mockImplementation(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
communityNodeType: vi.fn(),
|
||||||
|
...mock,
|
||||||
|
}) as unknown as NodeTypesStore,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('fetchInstalledPackageInfo', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createTestingPinia({ stubActions: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if no installed package is found', async () => {
|
||||||
|
const packageName = 'test-package';
|
||||||
|
mockCommunityNodesStore({
|
||||||
|
getInstalledPackage: vi.fn().mockResolvedValue(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fetchInstalledPackageInfo(packageName);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return package info with unverifiedUpdate as false if no update is available', async () => {
|
||||||
|
const packageName = 'test-package';
|
||||||
|
const installedPackage = { packageName, updateAvailable: null };
|
||||||
|
mockCommunityNodesStore({
|
||||||
|
getInstalledPackage: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(installedPackage as unknown as PublicInstalledPackage),
|
||||||
|
});
|
||||||
|
mockNodeTypesStore({
|
||||||
|
communityNodeType: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ npmVersion: '1.0.0' } as unknown as CommunityNodeType),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fetchInstalledPackageInfo(packageName);
|
||||||
|
expect(result).toEqual({ ...installedPackage, unverifiedUpdate: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return package info with unverifiedUpdate as true if an update is available', async () => {
|
||||||
|
const packageName = 'test-package';
|
||||||
|
const installedPackage = { packageName, updateAvailable: '1.1.0' };
|
||||||
|
mockCommunityNodesStore({
|
||||||
|
getInstalledPackage: vi
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(installedPackage as unknown as PublicInstalledPackage),
|
||||||
|
});
|
||||||
|
mockNodeTypesStore({
|
||||||
|
communityNodeType: vi
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue({ npmVersion: '1.0.0' } as unknown as CommunityNodeType),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fetchInstalledPackageInfo(packageName);
|
||||||
|
expect(result).toEqual({ ...installedPackage, unverifiedUpdate: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { useCommunityNodesStore } from '@/stores/communityNodes.store';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { type PublicInstalledPackage } from 'n8n-workflow';
|
||||||
|
import semver from 'semver';
|
||||||
|
|
||||||
|
export type ExtendedPublicInstalledPackage = PublicInstalledPackage & {
|
||||||
|
unverifiedUpdate: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchInstalledPackageInfo(
|
||||||
|
packageName: string,
|
||||||
|
): Promise<ExtendedPublicInstalledPackage | undefined> {
|
||||||
|
const installedPackage: PublicInstalledPackage | undefined =
|
||||||
|
await useCommunityNodesStore().getInstalledPackage(packageName);
|
||||||
|
const communityNodeType = useNodeTypesStore().communityNodeType(packageName);
|
||||||
|
if (!installedPackage) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const checkIsUnverifiedUpdate = () => {
|
||||||
|
if (!installedPackage?.updateAvailable || !communityNodeType) return false;
|
||||||
|
return semver.gt(installedPackage.updateAvailable, communityNodeType.npmVersion);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...installedPackage, unverifiedUpdate: checkIsUnverifiedUpdate() };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user