mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat: New package version available callout (#17097)
This commit is contained in:
@@ -17,4 +17,5 @@ export type CommunityNodeType = {
|
||||
companyName?: string;
|
||||
nodeDescription: INodeTypeDescription;
|
||||
isInstalled: boolean;
|
||||
nodeVersions?: Array<{ npmVersion: string; checksum: string }>;
|
||||
};
|
||||
|
||||
@@ -2,31 +2,16 @@ import type { CommunityNodeType } from '@n8n/api-types';
|
||||
import { Logger, inProduction } from '@n8n/backend-common';
|
||||
import { GlobalConfig } from '@n8n/config';
|
||||
import { Service } from '@n8n/di';
|
||||
import { ensureError, type INodeTypeDescription } from 'n8n-workflow';
|
||||
import { ensureError } from 'n8n-workflow';
|
||||
|
||||
import { CommunityPackagesService } from './community-packages.service';
|
||||
import { getCommunityNodeTypes } from '../utils/community-node-types-utils';
|
||||
import {
|
||||
getCommunityNodeTypes,
|
||||
StrapiCommunityNodeType,
|
||||
} from '../utils/community-node-types-utils';
|
||||
|
||||
const UPDATE_INTERVAL = 8 * 60 * 60 * 1000;
|
||||
|
||||
export type StrapiCommunityNodeType = {
|
||||
authorGithubUrl: string;
|
||||
authorName: string;
|
||||
checksum: string;
|
||||
description: string;
|
||||
displayName: string;
|
||||
name: string;
|
||||
numberOfStars: number;
|
||||
numberOfDownloads: number;
|
||||
packageName: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
npmVersion: string;
|
||||
isOfficialNode: boolean;
|
||||
companyName?: string;
|
||||
nodeDescription: INodeTypeDescription;
|
||||
};
|
||||
|
||||
@Service()
|
||||
export class CommunityNodeTypesService {
|
||||
private communityNodeTypes: Map<string, StrapiCommunityNodeType> = new Map();
|
||||
|
||||
@@ -18,6 +18,7 @@ export type StrapiCommunityNodeType = {
|
||||
isOfficialNode: boolean;
|
||||
companyName?: string;
|
||||
nodeDescription: INodeTypeDescription;
|
||||
nodeVersions?: Array<{ npmVersion: string; checksum: string }>;
|
||||
};
|
||||
|
||||
const N8N_VETTED_NODE_TYPES_STAGING_URL = 'https://api-staging.n8n.io/api/community-nodes';
|
||||
|
||||
@@ -3279,6 +3279,7 @@
|
||||
"communityNodeInfo.downloads": "{downloads} Downloads",
|
||||
"communityNodeInfo.publishedBy": "Published by {publisherName}",
|
||||
"communityNodeInfo.contact.admin": "Please contact an administrator to install this community node:",
|
||||
"communityNodeUpdateInfo.available": "A new node package version is available",
|
||||
"insights.upgradeModal.button.dismiss": "Dismiss",
|
||||
"insights.upgradeModal.button.upgrade": "Upgrade",
|
||||
"insights.upgradeModal.content": "Viewing this time period requires an enterprise plan. Upgrade to Enterprise to unlock advanced features.",
|
||||
|
||||
@@ -4,10 +4,14 @@ import { setActivePinia } from 'pinia';
|
||||
import CommunityNodeInfo from './CommunityNodeInfo.vue';
|
||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||
import { waitFor } from '@testing-library/vue';
|
||||
import type { CommunityNodeDetails } from '../composables/useViewStacks';
|
||||
|
||||
const getCommunityNodeAttributes = vi.fn();
|
||||
const communityNodesStore: { getInstalledPackages: PublicInstalledPackage[] } = {
|
||||
getInstalledPackages: [],
|
||||
const getInstalledPackage = vi.fn();
|
||||
const communityNodesStore: {
|
||||
getInstalledPackage: (packageName: string) => Promise<PublicInstalledPackage>;
|
||||
} = {
|
||||
getInstalledPackage,
|
||||
};
|
||||
|
||||
vi.mock('@/stores/nodeTypes.store', () => ({
|
||||
@@ -27,41 +31,7 @@ vi.mock('@/stores/communityNodes.store', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../composables/useViewStacks', () => ({
|
||||
useViewStacks: vi.fn(() => ({
|
||||
activeViewStack: {
|
||||
communityNodeDetails: {
|
||||
description: 'Other node description',
|
||||
installed: false,
|
||||
key: 'n8n-nodes-preview-test.OtherNode',
|
||||
nodeIcon: undefined,
|
||||
packageName: 'n8n-nodes-test',
|
||||
title: 'Other Node',
|
||||
},
|
||||
hasSearch: false,
|
||||
items: [
|
||||
{
|
||||
key: 'n8n-nodes-preview-test.OtherNode',
|
||||
properties: {
|
||||
defaults: {
|
||||
name: 'OtherNode',
|
||||
},
|
||||
description: 'Other node description',
|
||||
displayName: 'Other Node',
|
||||
group: ['transform'],
|
||||
name: 'n8n-nodes-preview-test.OtherNode',
|
||||
outputs: ['main'],
|
||||
},
|
||||
subcategory: '*',
|
||||
type: 'node',
|
||||
uuid: 'n8n-nodes-preview-test.OtherNode-32f238f0-2b05-47ce-b43d-7fab6d7ba3cb',
|
||||
},
|
||||
],
|
||||
mode: 'community-node',
|
||||
rootView: undefined,
|
||||
subcategory: 'Other Node',
|
||||
title: 'Node details',
|
||||
},
|
||||
})),
|
||||
useViewStacks: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('CommunityNodeInfo', () => {
|
||||
@@ -69,12 +39,51 @@ describe('CommunityNodeInfo', () => {
|
||||
let pinia: TestingPinia;
|
||||
let originalFetch: typeof global.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
const defaultViewStack = {
|
||||
communityNodeDetails: {
|
||||
description: 'Other node description',
|
||||
installed: false,
|
||||
key: 'n8n-nodes-preview-test.OtherNode',
|
||||
nodeIcon: undefined,
|
||||
packageName: 'n8n-nodes-test',
|
||||
title: 'Other Node',
|
||||
},
|
||||
hasSearch: false,
|
||||
items: [
|
||||
{
|
||||
key: 'n8n-nodes-preview-test.OtherNode',
|
||||
properties: {
|
||||
defaults: {
|
||||
name: 'OtherNode',
|
||||
},
|
||||
description: 'Other node description',
|
||||
displayName: 'Other Node',
|
||||
group: ['transform'],
|
||||
name: 'n8n-nodes-preview-test.OtherNode',
|
||||
outputs: ['main'],
|
||||
},
|
||||
subcategory: '*',
|
||||
type: 'node',
|
||||
uuid: 'n8n-nodes-preview-test.OtherNode-32f238f0-2b05-47ce-b43d-7fab6d7ba3cb',
|
||||
},
|
||||
],
|
||||
mode: 'community-node',
|
||||
rootView: undefined,
|
||||
subcategory: 'Other Node',
|
||||
title: 'Node details',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
originalFetch = global.fetch;
|
||||
global.fetch = vi.fn();
|
||||
|
||||
const { useViewStacks } = await import('../composables/useViewStacks');
|
||||
vi.mocked(useViewStacks).mockReturnValue({
|
||||
activeViewStack: defaultViewStack,
|
||||
} as ReturnType<typeof useViewStacks>);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -87,13 +96,13 @@ describe('CommunityNodeInfo', () => {
|
||||
npmVersion: '1.0.0',
|
||||
authorName: 'contributor',
|
||||
numberOfDownloads: 9999,
|
||||
nodeVersions: [{ npmVersion: '1.0.0' }],
|
||||
});
|
||||
communityNodesStore.getInstalledPackages = [
|
||||
{
|
||||
installedVersion: '1.0.0',
|
||||
packageName: 'n8n-nodes-test',
|
||||
} as PublicInstalledPackage,
|
||||
];
|
||||
getInstalledPackage.mockResolvedValue({
|
||||
installedVersion: '1.0.0',
|
||||
packageName: 'n8n-nodes-test',
|
||||
} as PublicInstalledPackage);
|
||||
|
||||
const wrapper = renderComponent({ pinia });
|
||||
|
||||
await waitFor(() => expect(wrapper.queryByTestId('number-of-downloads')).toBeInTheDocument());
|
||||
@@ -106,6 +115,45 @@ describe('CommunityNodeInfo', () => {
|
||||
expect(wrapper.getByTestId('publisher-name').textContent).toEqual('Published by contributor');
|
||||
});
|
||||
|
||||
it('should display update notice, should show verified batch for older versions', 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' }],
|
||||
});
|
||||
getInstalledPackage.mockResolvedValue({
|
||||
installedVersion: '0.0.9',
|
||||
packageName: 'n8n-nodes-test',
|
||||
updateAvailable: '1.0.1',
|
||||
} as PublicInstalledPackage);
|
||||
|
||||
const wrapper = renderComponent({ pinia });
|
||||
|
||||
await waitFor(() => expect(wrapper.queryByTestId('number-of-downloads')).toBeInTheDocument());
|
||||
|
||||
expect(wrapper.container.querySelector('.description')?.textContent).toEqual(
|
||||
'Other node description',
|
||||
);
|
||||
expect(wrapper.getByTestId('verified-tag').textContent).toEqual('Verified');
|
||||
expect(wrapper.getByTestId('number-of-downloads').textContent).toEqual('9,999 Downloads');
|
||||
expect(wrapper.getByTestId('publisher-name').textContent).toEqual('Published by contributor');
|
||||
expect(wrapper.getByTestId('update-available').textContent).toEqual(
|
||||
'A new node package version is available',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render correctly with fetched info', async () => {
|
||||
const packageData = {
|
||||
maintainers: [{ name: 'testAuthor' }],
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useCommunityNodesStore } from '@/stores/communityNodes.store';
|
||||
import { captureException } from '@sentry/vue';
|
||||
import { N8nText, N8nTooltip, N8nIcon } from '@n8n/design-system';
|
||||
import ShieldIcon from 'virtual:icons/fa-solid/shield-alt';
|
||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||
|
||||
const { activeViewStack } = useViewStacks();
|
||||
|
||||
@@ -21,6 +22,8 @@ const publisherName = ref<string | undefined>(undefined);
|
||||
const downloads = ref<string | null>(null);
|
||||
const verified = ref(false);
|
||||
const official = ref(false);
|
||||
const installedPackage = ref<PublicInstalledPackage | undefined>(undefined);
|
||||
|
||||
const communityNodesStore = useCommunityNodesStore();
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
|
||||
@@ -42,17 +45,22 @@ async function fetchPackageInfo(packageName: string) {
|
||||
activeViewStack.communityNodeDetails?.key || '',
|
||||
);
|
||||
|
||||
if (communityNodeDetails?.installed) {
|
||||
installedPackage.value = await communityNodesStore.getInstalledPackage(
|
||||
communityNodeDetails.packageName,
|
||||
);
|
||||
}
|
||||
|
||||
if (communityNodeAttributes) {
|
||||
publisherName.value = communityNodeAttributes.companyName ?? communityNodeAttributes.authorName;
|
||||
downloads.value = formatNumber(communityNodeAttributes.numberOfDownloads);
|
||||
official.value = communityNodeAttributes.isOfficialNode;
|
||||
const packageInfo = communityNodesStore.getInstalledPackages.find(
|
||||
(p) => p.packageName === communityNodeAttributes.packageName,
|
||||
);
|
||||
if (!packageInfo) {
|
||||
|
||||
if (!installedPackage.value) {
|
||||
verified.value = true;
|
||||
} else {
|
||||
verified.value = packageInfo.installedVersion === communityNodeAttributes.npmVersion;
|
||||
const verifiedVersions = communityNodeAttributes.nodeVersions?.map((v) => v.npmVersion) ?? [];
|
||||
verified.value = verifiedVersions.includes(installedPackage.value.installedVersion);
|
||||
}
|
||||
|
||||
return;
|
||||
@@ -107,7 +115,11 @@ onMounted(async () => {
|
||||
<N8nText :class="$style.description" color="text-base" size="medium">
|
||||
{{ communityNodeDetails?.description }}
|
||||
</N8nText>
|
||||
<div :class="$style.separator"></div>
|
||||
<CommunityNodeUpdateInfo
|
||||
v-if="isOwner && installedPackage?.updateAvailable"
|
||||
data-test-id="update-available"
|
||||
/>
|
||||
<div v-else :class="$style.separator"></div>
|
||||
<div :class="$style.info">
|
||||
<N8nTooltip v-if="verified" placement="top">
|
||||
<template #content>{{
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { N8nNotice } from '@n8n/design-system';
|
||||
import { i18n } from '@n8n/i18n';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const noticeStyles = computed(() => {
|
||||
const isDark = useUIStore().appliedTheme === 'dark';
|
||||
if (isDark) {
|
||||
return {
|
||||
borderColor: 'var(--color-callout-secondary-border)',
|
||||
backgroundColor: 'var(--color-callout-secondary-background)',
|
||||
color: 'var(--color-callout-secondary-font)',
|
||||
};
|
||||
}
|
||||
return {
|
||||
borderColor: 'var(--color-secondary)',
|
||||
backgroundColor: 'var(--color-secondary-tint-3)',
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<N8nNotice
|
||||
theme="info"
|
||||
:style="{
|
||||
marginTop: '0',
|
||||
...noticeStyles,
|
||||
}"
|
||||
>
|
||||
{{ i18n.baseText('communityNodeUpdateInfo.available') }}
|
||||
</N8nNotice>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module></style>
|
||||
@@ -96,8 +96,17 @@ export const useCommunityNodesStore = defineStore(STORES.COMMUNITY_NODES, () =>
|
||||
updatePackageObject(updatedPackage);
|
||||
};
|
||||
|
||||
const getInstalledPackage = async (packageName: string) => {
|
||||
if (!getInstalledPackages.value.length) {
|
||||
await fetchInstalledPackages();
|
||||
}
|
||||
|
||||
return getInstalledPackages.value.find((p) => p.packageName === packageName);
|
||||
};
|
||||
|
||||
return {
|
||||
installedPackages,
|
||||
getInstalledPackage,
|
||||
getInstalledPackages,
|
||||
availablePackageCount,
|
||||
fetchAvailableCommunityPackageCount,
|
||||
|
||||
Reference in New Issue
Block a user