mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Distinguish official verified nodes from community built nodes (#15630)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
|
||||||
export interface CommunityNodeAttributes {
|
export type CommunityNodeType = {
|
||||||
authorGithubUrl: string;
|
authorGithubUrl: string;
|
||||||
authorName: string;
|
authorName: string;
|
||||||
checksum: string;
|
checksum: string;
|
||||||
@@ -13,11 +13,8 @@ export interface CommunityNodeAttributes {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
npmVersion: string;
|
npmVersion: string;
|
||||||
}
|
isOfficialNode: boolean;
|
||||||
|
companyName?: string;
|
||||||
export interface CommunityNodeData {
|
nodeDescription: INodeTypeDescription;
|
||||||
id: number;
|
isInstalled: boolean;
|
||||||
attributes: CommunityNodeAttributes & {
|
};
|
||||||
nodeDescription: INodeTypeDescription;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { CommunityNodeAttributes } from '@n8n/api-types';
|
import type { CommunityNodeType } from '@n8n/api-types';
|
||||||
import type { InstalledPackages } from '@n8n/db';
|
import type { InstalledPackages } from '@n8n/db';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { CommunityPackagesController } from '@/controllers/community-packages.controller';
|
import { CommunityPackagesController } from '@/controllers/community-packages.controller';
|
||||||
import type { NodeRequest } from '@/requests';
|
import type { NodeRequest } from '@/requests';
|
||||||
@@ -46,7 +45,7 @@ describe('CommunityPackagesController', () => {
|
|||||||
body: { name: 'n8n-nodes-test', verify: true, version: '1.0.0' },
|
body: { name: 'n8n-nodes-test', verify: true, version: '1.0.0' },
|
||||||
});
|
});
|
||||||
communityNodeTypesService.findVetted.mockReturnValue(
|
communityNodeTypesService.findVetted.mockReturnValue(
|
||||||
mock<CommunityNodeAttributes & { nodeDescription: INodeTypeDescription }>({
|
mock<CommunityNodeType>({
|
||||||
checksum: 'checksum',
|
checksum: 'checksum',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CommunityNodeAttributes } from '@n8n/api-types';
|
import type { CommunityNodeType } from '@n8n/api-types';
|
||||||
import { Get, RestController } from '@n8n/decorators';
|
import { Get, RestController } from '@n8n/decorators';
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
|
|
||||||
@@ -9,12 +9,12 @@ export class CommunityNodeTypesController {
|
|||||||
constructor(private readonly communityNodeTypesService: CommunityNodeTypesService) {}
|
constructor(private readonly communityNodeTypesService: CommunityNodeTypesService) {}
|
||||||
|
|
||||||
@Get('/:name')
|
@Get('/:name')
|
||||||
async getCommunityNodeAttributes(req: Request): Promise<CommunityNodeAttributes | null> {
|
async getCommunityNodeType(req: Request): Promise<CommunityNodeType | null> {
|
||||||
return this.communityNodeTypesService.getCommunityNodeAttributes(req.params.name);
|
return await this.communityNodeTypesService.getCommunityNodeType(req.params.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/')
|
@Get('/')
|
||||||
async getCommunityNodeTypes() {
|
async getCommunityNodeTypes() {
|
||||||
return await this.communityNodeTypesService.getDescriptions();
|
return await this.communityNodeTypesService.getCommunityNodeTypes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,35 @@
|
|||||||
import type { CommunityNodeAttributes, CommunityNodeData } from '@n8n/api-types';
|
import type { CommunityNodeType } from '@n8n/api-types';
|
||||||
import { GlobalConfig } from '@n8n/config';
|
import { GlobalConfig } from '@n8n/config';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { Logger } from 'n8n-core';
|
import { Logger } from 'n8n-core';
|
||||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
import { ensureError, type INodeTypeDescription } from 'n8n-workflow';
|
||||||
import { ensureError } from 'n8n-workflow';
|
|
||||||
|
|
||||||
import { CommunityPackagesService } from './community-packages.service';
|
import { CommunityPackagesService } from './community-packages.service';
|
||||||
import { paginatedRequest } from '../utils/community-nodes-request-utils';
|
import { getCommunityNodeTypes } from '../utils/community-node-types-utils';
|
||||||
|
|
||||||
const UPDATE_INTERVAL = 8 * 60 * 60 * 1000;
|
const UPDATE_INTERVAL = 8 * 60 * 60 * 1000;
|
||||||
|
|
||||||
const N8N_VETTED_NODE_TYPES_STAGING_URL = 'https://api-staging.n8n.io/api/community-nodes';
|
export type StrapiCommunityNodeType = {
|
||||||
const N8N_VETTED_NODE_TYPES_PRODUCTION_URL = 'https://api.n8n.io/api/community-nodes';
|
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()
|
@Service()
|
||||||
export class CommunityNodeTypesService {
|
export class CommunityNodeTypesService {
|
||||||
private communityNodes: {
|
private communityNodeTypes: Map<string, StrapiCommunityNodeType> = new Map();
|
||||||
[key: string]: CommunityNodeAttributes & {
|
|
||||||
nodeDescription: INodeTypeDescription;
|
|
||||||
};
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
private lastUpdateTimestamp = 0;
|
private lastUpdateTimestamp = 0;
|
||||||
|
|
||||||
@@ -31,39 +41,33 @@ export class CommunityNodeTypesService {
|
|||||||
|
|
||||||
private async fetchNodeTypes() {
|
private async fetchNodeTypes() {
|
||||||
try {
|
try {
|
||||||
let data: CommunityNodeData[] = [];
|
let data: StrapiCommunityNodeType[] = [];
|
||||||
if (
|
if (
|
||||||
this.globalConfig.nodes.communityPackages.enabled &&
|
this.globalConfig.nodes.communityPackages.enabled &&
|
||||||
this.globalConfig.nodes.communityPackages.verifiedEnabled
|
this.globalConfig.nodes.communityPackages.verifiedEnabled
|
||||||
) {
|
) {
|
||||||
const environment = this.globalConfig.license.tenantId === 1 ? 'production' : 'staging';
|
const environment = this.globalConfig.license.tenantId === 1 ? 'production' : 'staging';
|
||||||
const url =
|
data = await getCommunityNodeTypes(environment);
|
||||||
environment === 'production'
|
|
||||||
? N8N_VETTED_NODE_TYPES_PRODUCTION_URL
|
|
||||||
: N8N_VETTED_NODE_TYPES_STAGING_URL;
|
|
||||||
data = await paginatedRequest(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateData(data);
|
this.updateCommunityNodeTypes(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error('Failed to fetch community node types', { error: ensureError(error) });
|
this.logger.error('Failed to fetch community node types', { error: ensureError(error) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateData(data: CommunityNodeData[]) {
|
private updateCommunityNodeTypes(nodeTypes: StrapiCommunityNodeType[]) {
|
||||||
if (!data?.length) return;
|
if (!nodeTypes?.length) return;
|
||||||
|
|
||||||
this.resetData();
|
this.resetCommunityNodeTypes();
|
||||||
|
|
||||||
for (const entry of data) {
|
this.communityNodeTypes = new Map(nodeTypes.map((nodeType) => [nodeType.name, nodeType]));
|
||||||
this.communityNodes[entry.attributes.name] = entry.attributes;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.lastUpdateTimestamp = Date.now();
|
this.lastUpdateTimestamp = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
private resetData() {
|
private resetCommunityNodeTypes() {
|
||||||
this.communityNodes = {};
|
this.communityNodeTypes = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateRequired() {
|
private updateRequired() {
|
||||||
@@ -71,36 +75,37 @@ export class CommunityNodeTypesService {
|
|||||||
return Date.now() - this.lastUpdateTimestamp > UPDATE_INTERVAL;
|
return Date.now() - this.lastUpdateTimestamp > UPDATE_INTERVAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDescriptions(): Promise<INodeTypeDescription[]> {
|
private async createIsInstalled() {
|
||||||
const nodesDescriptions: INodeTypeDescription[] = [];
|
const installedPackages = (await this.communityPackagesService.getAllInstalledPackages()) ?? [];
|
||||||
|
const installedPackageNames = new Set(installedPackages.map((p) => p.packageName));
|
||||||
|
|
||||||
if (this.updateRequired() || !Object.keys(this.communityNodes).length) {
|
return (nodeTypeName: string) => installedPackageNames.has(nodeTypeName.split('.')[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCommunityNodeTypes(): Promise<CommunityNodeType[]> {
|
||||||
|
if (this.updateRequired() || !this.communityNodeTypes.size) {
|
||||||
await this.fetchNodeTypes();
|
await this.fetchNodeTypes();
|
||||||
}
|
}
|
||||||
|
|
||||||
const installedPackages = (
|
const isInstalled = await this.createIsInstalled();
|
||||||
(await this.communityPackagesService.getAllInstalledPackages()) ?? []
|
|
||||||
).map((p) => p.packageName);
|
|
||||||
|
|
||||||
for (const node of Object.values(this.communityNodes)) {
|
return Array.from(this.communityNodeTypes.values()).map((nodeType) => ({
|
||||||
if (installedPackages.includes(node.name.split('.')[0])) continue;
|
...nodeType,
|
||||||
nodesDescriptions.push(node.nodeDescription);
|
isInstalled: isInstalled(nodeType.name),
|
||||||
}
|
}));
|
||||||
|
|
||||||
return nodesDescriptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommunityNodeAttributes(type: string): CommunityNodeAttributes | null {
|
async getCommunityNodeType(type: string): Promise<CommunityNodeType | null> {
|
||||||
const node = this.communityNodes[type];
|
const nodeType = this.communityNodeTypes.get(type);
|
||||||
if (!node) return null;
|
const isInstalled = await this.createIsInstalled();
|
||||||
const { nodeDescription, ...attributes } = node;
|
if (!nodeType) return null;
|
||||||
return attributes;
|
return { ...nodeType, isInstalled: isInstalled(nodeType.name) };
|
||||||
}
|
}
|
||||||
|
|
||||||
findVetted(packageName: string) {
|
findVetted(packageName: string) {
|
||||||
const vettedTypes = Object.keys(this.communityNodes);
|
const vettedTypes = Array.from(this.communityNodeTypes.keys());
|
||||||
const nodeName = vettedTypes.find((t) => t.includes(packageName));
|
const nodeName = vettedTypes.find((t) => t.includes(packageName));
|
||||||
if (!nodeName) return;
|
if (!nodeName) return;
|
||||||
return this.communityNodes[nodeName];
|
return this.communityNodeTypes.get(nodeName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
import type { CommunityNodeData } from '@n8n/api-types';
|
|
||||||
import nock from 'nock';
|
|
||||||
|
|
||||||
import { paginatedRequest } from '../community-nodes-request-utils';
|
|
||||||
|
|
||||||
describe('strapiPaginatedRequest', () => {
|
|
||||||
const baseUrl = 'https://strapi.test/api/nodes';
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
nock.cleanAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fetch and combine multiple pages of data', async () => {
|
|
||||||
const page1 = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
attributes: { name: 'Node1', nodeDescription: { name: 'n1', version: 1 } } as any,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const page2 = [
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
attributes: { name: 'Node2', nodeDescription: { name: 'n2', version: 2 } } as any,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
nock('https://strapi.test')
|
|
||||||
.get('/api/nodes')
|
|
||||||
.query(true)
|
|
||||||
.reply(200, {
|
|
||||||
data: page1,
|
|
||||||
meta: {
|
|
||||||
pagination: {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 25,
|
|
||||||
pageCount: 2,
|
|
||||||
total: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
nock('https://strapi.test')
|
|
||||||
.get('/api/nodes')
|
|
||||||
.query(true)
|
|
||||||
.reply(200, {
|
|
||||||
data: page2,
|
|
||||||
meta: {
|
|
||||||
pagination: {
|
|
||||||
page: 2,
|
|
||||||
pageSize: 25,
|
|
||||||
pageCount: 2,
|
|
||||||
total: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await paginatedRequest('https://strapi.test/api/nodes');
|
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
|
||||||
expect(result[0].id).toBe(1);
|
|
||||||
expect(result[1].id).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return empty array if no data', async () => {
|
|
||||||
nock('https://strapi.test')
|
|
||||||
.get('/api/nodes')
|
|
||||||
.query(true)
|
|
||||||
.reply(200, {
|
|
||||||
data: [],
|
|
||||||
meta: {
|
|
||||||
pagination: {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 25,
|
|
||||||
pageCount: 0,
|
|
||||||
total: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await paginatedRequest(baseUrl);
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return single page data', async () => {
|
|
||||||
const singlePage: CommunityNodeData[] = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
attributes: {
|
|
||||||
name: 'NodeSingle',
|
|
||||||
nodeDescription: { name: 'n1', version: 1 },
|
|
||||||
} as any,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
nock('https://strapi.test')
|
|
||||||
.get('/api/nodes')
|
|
||||||
.query(true)
|
|
||||||
.reply(200, {
|
|
||||||
data: singlePage,
|
|
||||||
meta: {
|
|
||||||
pagination: {
|
|
||||||
page: 1,
|
|
||||||
pageSize: 25,
|
|
||||||
pageCount: 1,
|
|
||||||
total: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await paginatedRequest(baseUrl);
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result[0].attributes.name).toBe('NodeSingle');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an empty array if the request fails', async () => {
|
|
||||||
const endpoint = '/nodes';
|
|
||||||
|
|
||||||
nock(baseUrl).get(endpoint).query(true).replyWithError('Request failed');
|
|
||||||
|
|
||||||
const result = await paginatedRequest(`${baseUrl}${endpoint}`);
|
|
||||||
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
128
packages/cli/src/utils/__tests__/strapi-utils.test.ts
Normal file
128
packages/cli/src/utils/__tests__/strapi-utils.test.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import nock from 'nock';
|
||||||
|
|
||||||
|
import { paginatedRequest } from '../strapi-utils';
|
||||||
|
|
||||||
|
describe('Strapi utils', () => {
|
||||||
|
describe('paginatedRequest', () => {
|
||||||
|
const baseUrl = 'https://strapi.test/api/nodes';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
nock.cleanAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch and combine multiple pages of data', async () => {
|
||||||
|
const page1 = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
attributes: { name: 'Node1', nodeDescription: { name: 'n1', version: 1 } },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const page2 = [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
attributes: { name: 'Node2', nodeDescription: { name: 'n2', version: 2 } },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
nock('https://strapi.test')
|
||||||
|
.get('/api/nodes')
|
||||||
|
.query(true)
|
||||||
|
.reply(200, {
|
||||||
|
data: page1,
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
pageCount: 2,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
nock('https://strapi.test')
|
||||||
|
.get('/api/nodes')
|
||||||
|
.query(true)
|
||||||
|
.reply(200, {
|
||||||
|
data: page2,
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: 2,
|
||||||
|
pageSize: 25,
|
||||||
|
pageCount: 2,
|
||||||
|
total: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await paginatedRequest<(typeof page1)[number]['attributes']>(
|
||||||
|
'https://strapi.test/api/nodes',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0].name).toBe('Node1');
|
||||||
|
expect(result[1].name).toBe('Node2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array if no data', async () => {
|
||||||
|
nock('https://strapi.test')
|
||||||
|
.get('/api/nodes')
|
||||||
|
.query(true)
|
||||||
|
.reply(200, {
|
||||||
|
data: [],
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
pageCount: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await paginatedRequest(baseUrl);
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return single page data', async () => {
|
||||||
|
const singlePage = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
attributes: {
|
||||||
|
name: 'NodeSingle',
|
||||||
|
nodeDescription: { name: 'n1', version: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
nock('https://strapi.test')
|
||||||
|
.get('/api/nodes')
|
||||||
|
.query(true)
|
||||||
|
.reply(200, {
|
||||||
|
data: singlePage,
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
pageCount: 1,
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await paginatedRequest<(typeof singlePage)[number]['attributes']>(baseUrl);
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result[0].name).toBe('NodeSingle');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array if the request fails', async () => {
|
||||||
|
const endpoint = '/nodes';
|
||||||
|
|
||||||
|
nock(baseUrl).get(endpoint).query(true).replyWithError('Request failed');
|
||||||
|
|
||||||
|
const result = await paginatedRequest(`${baseUrl}${endpoint}`);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
35
packages/cli/src/utils/community-node-types-utils.ts
Normal file
35
packages/cli/src/utils/community-node-types-utils.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { paginatedRequest } from './strapi-utils';
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const N8N_VETTED_NODE_TYPES_STAGING_URL = 'https://api-staging.n8n.io/api/community-nodes';
|
||||||
|
const N8N_VETTED_NODE_TYPES_PRODUCTION_URL = 'https://api.n8n.io/api/community-nodes';
|
||||||
|
|
||||||
|
export async function getCommunityNodeTypes(
|
||||||
|
environment: 'staging' | 'production',
|
||||||
|
): Promise<StrapiCommunityNodeType[]> {
|
||||||
|
const url =
|
||||||
|
environment === 'production'
|
||||||
|
? N8N_VETTED_NODE_TYPES_PRODUCTION_URL
|
||||||
|
: N8N_VETTED_NODE_TYPES_STAGING_URL;
|
||||||
|
|
||||||
|
return await paginatedRequest<StrapiCommunityNodeType>(url);
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { CommunityNodeData } from '@n8n/api-types';
|
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { ErrorReporter, Logger } from 'n8n-core';
|
import { ErrorReporter, Logger } from 'n8n-core';
|
||||||
|
|
||||||
interface ResponseData {
|
interface ResponseData<T> {
|
||||||
data: CommunityNodeData[];
|
data: Array<Entity<T>>;
|
||||||
meta: Meta;
|
meta: Meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,6 +11,11 @@ interface Meta {
|
|||||||
pagination: Pagination;
|
pagination: Pagination;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Entity<T> {
|
||||||
|
id: number;
|
||||||
|
attributes: T;
|
||||||
|
}
|
||||||
|
|
||||||
interface Pagination {
|
interface Pagination {
|
||||||
page: number;
|
page: number;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
@@ -19,9 +23,9 @@ interface Pagination {
|
|||||||
total: number;
|
total: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function paginatedRequest(url: string): Promise<CommunityNodeData[]> {
|
export async function paginatedRequest<T>(url: string): Promise<T[]> {
|
||||||
let returnData: CommunityNodeData[] = [];
|
let returnData: T[] = [];
|
||||||
let responseData: CommunityNodeData[] | undefined = [];
|
let responseData: T[] | undefined = [];
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
pagination: {
|
pagination: {
|
||||||
@@ -33,7 +37,7 @@ export async function paginatedRequest(url: string): Promise<CommunityNodeData[]
|
|||||||
do {
|
do {
|
||||||
let response;
|
let response;
|
||||||
try {
|
try {
|
||||||
response = await axios.get<ResponseData>(url, {
|
response = await axios.get<ResponseData<T>>(url, {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
@@ -47,7 +51,7 @@ export async function paginatedRequest(url: string): Promise<CommunityNodeData[]
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
responseData = response?.data?.data;
|
responseData = response?.data?.data?.map((item) => item.attributes);
|
||||||
|
|
||||||
if (!responseData?.length) break;
|
if (!responseData?.length) break;
|
||||||
|
|
||||||
@@ -4,7 +4,6 @@ import { ElTag } from 'element-plus';
|
|||||||
|
|
||||||
import { useI18n } from '../../composables/useI18n';
|
import { useI18n } from '../../composables/useI18n';
|
||||||
import type { NodeCreatorTag } from '../../types/node-creator-node';
|
import type { NodeCreatorTag } from '../../types/node-creator-node';
|
||||||
import N8nTooltip from '../N8nTooltip';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
active?: boolean;
|
active?: boolean;
|
||||||
@@ -14,6 +13,7 @@ export interface Props {
|
|||||||
tag?: NodeCreatorTag;
|
tag?: NodeCreatorTag;
|
||||||
title: string;
|
title: string;
|
||||||
showActionArrow?: boolean;
|
showActionArrow?: boolean;
|
||||||
|
isOfficial?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>();
|
defineProps<Props>();
|
||||||
@@ -22,6 +22,8 @@ defineEmits<{
|
|||||||
tooltipClick: [e: MouseEvent];
|
tooltipClick: [e: MouseEvent];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
defineSlots<{ icon: {}; extraDetails: {}; dragContent: {} }>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -49,16 +51,8 @@ const { t } = useI18n();
|
|||||||
:title="t('nodeCreator.nodeItem.triggerIconTitle')"
|
:title="t('nodeCreator.nodeItem.triggerIconTitle')"
|
||||||
:class="$style.triggerIcon"
|
:class="$style.triggerIcon"
|
||||||
/>
|
/>
|
||||||
<N8nTooltip
|
|
||||||
v-if="!!$slots.tooltip"
|
<slot name="extraDetails" />
|
||||||
placement="top"
|
|
||||||
data-test-id="node-creator-item-tooltip"
|
|
||||||
>
|
|
||||||
<template #content>
|
|
||||||
<slot name="tooltip" />
|
|
||||||
</template>
|
|
||||||
<n8n-icon :class="$style.tooltipIcon" icon="cube" />
|
|
||||||
</N8nTooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="description"
|
v-if="description"
|
||||||
@@ -121,7 +115,9 @@ const { t } = useI18n();
|
|||||||
width: 12px;
|
width: 12px;
|
||||||
}
|
}
|
||||||
.details {
|
.details {
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--spacing-3xs);
|
||||||
}
|
}
|
||||||
.nodeIcon {
|
.nodeIcon {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -141,12 +137,10 @@ const { t } = useI18n();
|
|||||||
}
|
}
|
||||||
|
|
||||||
.aiIcon {
|
.aiIcon {
|
||||||
margin-left: var(--spacing-3xs);
|
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.triggerIcon {
|
.triggerIcon {
|
||||||
margin-left: var(--spacing-3xs);
|
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import type { INodeTranslationHeaders, IRestApiContext } from '@/Interface';
|
||||||
|
import { makeRestApiRequest } from '@/utils/apiUtils';
|
||||||
import type {
|
import type {
|
||||||
ActionResultRequestDto,
|
ActionResultRequestDto,
|
||||||
|
CommunityNodeType,
|
||||||
OptionsRequestDto,
|
OptionsRequestDto,
|
||||||
ResourceLocatorRequestDto,
|
ResourceLocatorRequestDto,
|
||||||
ResourceMapperFieldsRequestDto,
|
ResourceMapperFieldsRequestDto,
|
||||||
} from '@n8n/api-types';
|
} from '@n8n/api-types';
|
||||||
import { makeRestApiRequest } from '@/utils/apiUtils';
|
import axios from 'axios';
|
||||||
import type { INodeTranslationHeaders, IRestApiContext } from '@/Interface';
|
|
||||||
import type { CommunityNodeAttributes } from '@n8n/api-types';
|
|
||||||
import {
|
import {
|
||||||
type INodeListSearchResult,
|
type INodeListSearchResult,
|
||||||
type INodePropertyOptions,
|
type INodePropertyOptions,
|
||||||
@@ -16,7 +17,6 @@ import {
|
|||||||
type ResourceMapperFields,
|
type ResourceMapperFields,
|
||||||
sleep,
|
sleep,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
async function fetchNodeTypesJsonWithRetry(url: string, retries = 5, delay = 500) {
|
async function fetchNodeTypesJsonWithRetry(url: string, retries = 5, delay = 500) {
|
||||||
for (let attempt = 0; attempt < retries; attempt++) {
|
for (let attempt = 0; attempt < retries; attempt++) {
|
||||||
@@ -38,14 +38,14 @@ export async function getNodeTypes(baseUrl: string) {
|
|||||||
|
|
||||||
export async function fetchCommunityNodeTypes(
|
export async function fetchCommunityNodeTypes(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
): Promise<INodeTypeDescription[]> {
|
): Promise<CommunityNodeType[]> {
|
||||||
return await makeRestApiRequest(context, 'GET', '/community-node-types');
|
return await makeRestApiRequest(context, 'GET', '/community-node-types');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchCommunityNodeAttributes(
|
export async function fetchCommunityNodeAttributes(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
type: string,
|
type: string,
|
||||||
): Promise<CommunityNodeAttributes | null> {
|
): Promise<CommunityNodeType | null> {
|
||||||
return await makeRestApiRequest(
|
return await makeRestApiRequest(
|
||||||
context,
|
context,
|
||||||
'GET',
|
'GET',
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import type { SimplifiedNodeType } from '@/Interface';
|
import type { SimplifiedNodeType } from '@/Interface';
|
||||||
import {
|
import {
|
||||||
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
|
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
|
||||||
@@ -8,17 +7,21 @@ import {
|
|||||||
DRAG_EVENT_DATA_KEY,
|
DRAG_EVENT_DATA_KEY,
|
||||||
HITL_SUBCATEGORY,
|
HITL_SUBCATEGORY,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
|
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
|
import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
|
||||||
|
import OfficialIcon from 'virtual:icons/mdi/verified';
|
||||||
|
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useNodeType } from '@/composables/useNodeType';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { N8nTooltip } from '@n8n/design-system';
|
||||||
import { useActions } from '../composables/useActions';
|
import { useActions } from '../composables/useActions';
|
||||||
import { useViewStacks } from '../composables/useViewStacks';
|
import { useViewStacks } from '../composables/useViewStacks';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { isNodePreviewKey, removePreviewToken } from '../utils';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
|
||||||
import { useNodeType } from '@/composables/useNodeType';
|
|
||||||
import { isNodePreviewKey } from '../utils';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
nodeType: SimplifiedNodeType;
|
nodeType: SimplifiedNodeType;
|
||||||
@@ -40,6 +43,7 @@ const { activeViewStack } = useViewStacks();
|
|||||||
const { isSubNodeType } = useNodeType({
|
const { isSubNodeType } = useNodeType({
|
||||||
nodeType: props.nodeType,
|
nodeType: props.nodeType,
|
||||||
});
|
});
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
|
||||||
const dragging = ref(false);
|
const dragging = ref(false);
|
||||||
const draggablePosition = ref({ x: -100, y: -100 });
|
const draggablePosition = ref({ x: -100, y: -100 });
|
||||||
@@ -108,6 +112,18 @@ const isTrigger = computed<boolean>(() => {
|
|||||||
return props.nodeType.group.includes('trigger') && !hasActions.value;
|
return props.nodeType.group.includes('trigger') && !hasActions.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const communityNodeType = computed(() => {
|
||||||
|
return nodeTypesStore.communityNodeType(removePreviewToken(props.nodeType.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOfficial = computed(() => {
|
||||||
|
return communityNodeType.value?.isOfficialNode ?? false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const author = computed(() => {
|
||||||
|
return communityNodeType.value?.displayName ?? displayName.value;
|
||||||
|
});
|
||||||
|
|
||||||
function onDragStart(event: DragEvent): void {
|
function onDragStart(event: DragEvent): void {
|
||||||
if (event.dataTransfer) {
|
if (event.dataTransfer) {
|
||||||
event.dataTransfer.effectAllowed = 'copy';
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
@@ -145,6 +161,7 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
|
|||||||
:title="displayName"
|
:title="displayName"
|
||||||
:show-action-arrow="showActionArrow"
|
:show-action-arrow="showActionArrow"
|
||||||
:is-trigger="isTrigger"
|
:is-trigger="isTrigger"
|
||||||
|
:is-official="isOfficial"
|
||||||
:data-test-id="dataTestId"
|
:data-test-id="dataTestId"
|
||||||
:tag="nodeType.tag"
|
:tag="nodeType.tag"
|
||||||
@dragstart="onDragStart"
|
@dragstart="onDragStart"
|
||||||
@@ -155,22 +172,38 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
|
|||||||
<NodeIcon :class="$style.nodeIcon" :node-type="nodeType" />
|
<NodeIcon :class="$style.nodeIcon" :node-type="nodeType" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-if="isOfficial" #extraDetails>
|
||||||
|
<N8nTooltip placement="top" :show-after="500">
|
||||||
|
<template #content>
|
||||||
|
{{ i18n.baseText('generic.officialNode.tooltip', { interpolate: { author: author } }) }}
|
||||||
|
</template>
|
||||||
|
<OfficialIcon :class="[$style.icon, $style.official]" />
|
||||||
|
</N8nTooltip>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template
|
<template
|
||||||
v-if="isCommunityNode && !isCommunityNodePreview && !activeViewStack?.communityNodeDetails"
|
v-else-if="
|
||||||
#tooltip
|
isCommunityNode && !isCommunityNodePreview && !activeViewStack?.communityNodeDetails
|
||||||
|
"
|
||||||
|
#extraDetails
|
||||||
>
|
>
|
||||||
<p
|
<N8nTooltip placement="top" :show-after="500">
|
||||||
v-n8n-html="
|
<template #content>
|
||||||
i18n.baseText('generic.communityNode.tooltip', {
|
<p
|
||||||
interpolate: {
|
v-n8n-html="
|
||||||
packageName: nodeType.name.split('.')[0],
|
i18n.baseText('generic.communityNode.tooltip', {
|
||||||
docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL,
|
interpolate: {
|
||||||
},
|
packageName: nodeType.name.split('.')[0],
|
||||||
})
|
docURL: COMMUNITY_NODES_INSTALLATION_DOCS_URL,
|
||||||
"
|
},
|
||||||
:class="$style.communityNodeIcon"
|
})
|
||||||
@click="onCommunityNodeTooltipClick"
|
"
|
||||||
/>
|
:class="$style.communityNodeIcon"
|
||||||
|
@click="onCommunityNodeTooltipClick"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<n8n-icon size="small" :class="$style.icon" icon="cube" />
|
||||||
|
</N8nTooltip>
|
||||||
</template>
|
</template>
|
||||||
<template #dragContent>
|
<template #dragContent>
|
||||||
<div
|
<div
|
||||||
@@ -230,4 +263,14 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
|
|||||||
width: 1px;
|
width: 1px;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: inline-flex;
|
||||||
|
color: var(--color-text-base);
|
||||||
|
width: 12px;
|
||||||
|
|
||||||
|
&.official {
|
||||||
|
width: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { createComponentRenderer } from '@/__tests__/render';
|
|||||||
import { type TestingPinia, createTestingPinia } from '@pinia/testing';
|
import { type TestingPinia, createTestingPinia } from '@pinia/testing';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
import CommunityNodeDetails from './CommunityNodeDetails.vue';
|
import CommunityNodeDetails from './CommunityNodeDetails.vue';
|
||||||
import { fireEvent, waitFor } from '@testing-library/vue';
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
import { userEvent } from '@testing-library/user-event';
|
||||||
|
|
||||||
const fetchCredentialTypes = vi.fn();
|
const fetchCredentialTypes = vi.fn();
|
||||||
const getCommunityNodeAttributes = vi.fn(() => ({ npmVersion: '1.0.0' }));
|
const getCommunityNodeAttributes = vi.fn(() => ({ npmVersion: '1.0.0' }));
|
||||||
@@ -55,6 +56,7 @@ vi.mock('@/stores/nodeTypes.store', () => ({
|
|||||||
useNodeTypesStore: vi.fn(() => ({
|
useNodeTypesStore: vi.fn(() => ({
|
||||||
getCommunityNodeAttributes,
|
getCommunityNodeAttributes,
|
||||||
getNodeTypes,
|
getNodeTypes,
|
||||||
|
communityNodeType: vi.fn(() => ({ isOfficialNode: true })),
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -108,7 +110,7 @@ vi.mock('../composables/useViewStacks', () => ({
|
|||||||
mode: 'community-node',
|
mode: 'community-node',
|
||||||
rootView: undefined,
|
rootView: undefined,
|
||||||
subcategory: 'Other Node',
|
subcategory: 'Other Node',
|
||||||
title: 'Community node details',
|
title: 'Node details',
|
||||||
},
|
},
|
||||||
pushViewStack,
|
pushViewStack,
|
||||||
popViewStack,
|
popViewStack,
|
||||||
@@ -135,9 +137,9 @@ describe('CommunityNodeDetails', () => {
|
|||||||
const installButton = wrapper.getByTestId('install-community-node-button');
|
const installButton = wrapper.getByTestId('install-community-node-button');
|
||||||
|
|
||||||
expect(wrapper.container.querySelector('.title span')?.textContent).toEqual('Other Node');
|
expect(wrapper.container.querySelector('.title span')?.textContent).toEqual('Other Node');
|
||||||
expect(installButton.querySelector('span')?.textContent).toEqual('Install Node');
|
expect(installButton.querySelector('span')?.textContent).toEqual('Install node');
|
||||||
|
|
||||||
await fireEvent.click(installButton);
|
await userEvent.click(installButton);
|
||||||
|
|
||||||
await waitFor(() => expect(removeNodeFromMergedNodes).toHaveBeenCalled());
|
await waitFor(() => expect(removeNodeFromMergedNodes).toHaveBeenCalled());
|
||||||
|
|
||||||
@@ -155,6 +157,7 @@ describe('CommunityNodeDetails', () => {
|
|||||||
nodeIcon: undefined,
|
nodeIcon: undefined,
|
||||||
packageName: 'n8n-nodes-test',
|
packageName: 'n8n-nodes-test',
|
||||||
title: 'Other Node',
|
title: 'Other Node',
|
||||||
|
official: true,
|
||||||
},
|
},
|
||||||
hasSearch: false,
|
hasSearch: false,
|
||||||
items: [
|
items: [
|
||||||
@@ -178,9 +181,10 @@ describe('CommunityNodeDetails', () => {
|
|||||||
mode: 'community-node',
|
mode: 'community-node',
|
||||||
rootView: undefined,
|
rootView: undefined,
|
||||||
subcategory: 'Other Node',
|
subcategory: 'Other Node',
|
||||||
title: 'Community node details',
|
title: 'Node details',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
resetStacks: true,
|
||||||
transitionDirection: 'none',
|
transitionDirection: 'none',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -196,7 +200,7 @@ describe('CommunityNodeDetails', () => {
|
|||||||
|
|
||||||
const installButton = wrapper.getByTestId('install-community-node-button');
|
const installButton = wrapper.getByTestId('install-community-node-button');
|
||||||
|
|
||||||
await fireEvent.click(installButton);
|
await userEvent.click(installButton);
|
||||||
|
|
||||||
expect(showError).toHaveBeenCalledWith(expect.any(Error), 'Error installing new package');
|
expect(showError).toHaveBeenCalledWith(expect.any(Error), 'Error installing new package');
|
||||||
expect(pushViewStack).not.toHaveBeenCalled();
|
expect(pushViewStack).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { i18n } from '@/plugins/i18n';
|
|||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
import { useNodeCreatorStore } from '@/stores/nodeCreator.store';
|
||||||
import { useCredentialsStore } from '@/stores/credentials.store';
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
|
import OfficialIcon from 'virtual:icons/mdi/verified';
|
||||||
|
|
||||||
import { getNodeIconSource } from '@/utils/nodeIcon';
|
import { getNodeIconSource } from '@/utils/nodeIcon';
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ const updateViewStack = (key: string) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
pushViewStack(viewStack, {
|
pushViewStack(viewStack, {
|
||||||
|
resetStacks: true,
|
||||||
transitionDirection: 'none',
|
transitionDirection: 'none',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -95,33 +97,50 @@ const onInstall = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.container">
|
<div v-if="communityNodeDetails" :class="$style.container">
|
||||||
<div :class="$style.header">
|
<div :class="$style.header">
|
||||||
<div :class="$style.title">
|
<div :class="$style.title">
|
||||||
<NodeIcon
|
<NodeIcon
|
||||||
v-if="communityNodeDetails?.nodeIcon"
|
v-if="communityNodeDetails.nodeIcon"
|
||||||
:class="$style.nodeIcon"
|
:class="$style.nodeIcon"
|
||||||
:icon-source="communityNodeDetails.nodeIcon"
|
:icon-source="communityNodeDetails.nodeIcon"
|
||||||
:circle="false"
|
:circle="false"
|
||||||
:show-tooltip="false"
|
:show-tooltip="false"
|
||||||
/>
|
/>
|
||||||
<span>{{ communityNodeDetails?.title }}</span>
|
<span>{{ communityNodeDetails.title }}</span>
|
||||||
|
<N8nTooltip v-if="communityNodeDetails.official" placement="bottom" :show-after="500">
|
||||||
|
<template #content>
|
||||||
|
{{
|
||||||
|
i18n.baseText('generic.officialNode.tooltip', {
|
||||||
|
interpolate: {
|
||||||
|
author: communityNodeDetails.companyName ?? communityNodeDetails.title,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<OfficialIcon :class="$style.officialIcon" />
|
||||||
|
</N8nTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="communityNodeDetails?.installed" :class="$style.installed">
|
<div v-if="communityNodeDetails.installed" :class="$style.installed">
|
||||||
<FontAwesomeIcon :class="$style.installedIcon" icon="cube" />
|
<FontAwesomeIcon
|
||||||
|
v-if="!communityNodeDetails.official"
|
||||||
|
:class="$style.installedIcon"
|
||||||
|
icon="cube"
|
||||||
|
/>
|
||||||
<N8nText color="text-light" size="small" bold>
|
<N8nText color="text-light" size="small" bold>
|
||||||
{{ i18n.baseText('communityNodeDetails.installed') }}
|
{{ i18n.baseText('communityNodeDetails.installed') }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<N8nButton
|
<N8nButton
|
||||||
v-else-if="isOwner"
|
v-if="isOwner && !communityNodeDetails.installed"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
label="Install Node"
|
:label="i18n.baseText('communityNodeDetails.install')"
|
||||||
size="small"
|
size="small"
|
||||||
@click="onInstall"
|
|
||||||
data-test-id="install-community-node-button"
|
data-test-id="install-community-node-button"
|
||||||
|
@click="onInstall"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,6 +157,7 @@ const onInstall = async () => {
|
|||||||
}
|
}
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
@@ -159,6 +179,14 @@ const onInstall = async () => {
|
|||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.officialIcon {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: var(--spacing-4xs);
|
||||||
|
color: var(--color-text-base);
|
||||||
|
width: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.installed {
|
.installed {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ vi.mock('../composables/useViewStacks', () => ({
|
|||||||
mode: 'community-node',
|
mode: 'community-node',
|
||||||
rootView: undefined,
|
rootView: undefined,
|
||||||
subcategory: 'Other Node',
|
subcategory: 'Other Node',
|
||||||
title: 'Community node details',
|
title: 'Node details',
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|||||||
import { useCommunityNodesStore } from '@/stores/communityNodes.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';
|
||||||
|
|
||||||
const { activeViewStack } = useViewStacks();
|
const { activeViewStack } = useViewStacks();
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ interface DownloadData {
|
|||||||
const publisherName = ref<string | undefined>(undefined);
|
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 communityNodesStore = useCommunityNodesStore();
|
const communityNodesStore = useCommunityNodesStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
|
||||||
@@ -41,8 +43,9 @@ async function fetchPackageInfo(packageName: string) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (communityNodeAttributes) {
|
if (communityNodeAttributes) {
|
||||||
publisherName.value = communityNodeAttributes.authorName;
|
publisherName.value = communityNodeAttributes.companyName ?? communityNodeAttributes.authorName;
|
||||||
downloads.value = formatNumber(communityNodeAttributes.numberOfDownloads);
|
downloads.value = formatNumber(communityNodeAttributes.numberOfDownloads);
|
||||||
|
official.value = communityNodeAttributes.isOfficialNode;
|
||||||
const packageInfo = communityNodesStore.getInstalledPackages.find(
|
const packageInfo = communityNodesStore.getInstalledPackages.find(
|
||||||
(p) => p.packageName === communityNodeAttributes.packageName,
|
(p) => p.packageName === communityNodeAttributes.packageName,
|
||||||
);
|
);
|
||||||
@@ -106,17 +109,21 @@ onMounted(async () => {
|
|||||||
</N8nText>
|
</N8nText>
|
||||||
<div :class="$style.separator"></div>
|
<div :class="$style.separator"></div>
|
||||||
<div :class="$style.info">
|
<div :class="$style.info">
|
||||||
<N8nTooltip placement="top" v-if="verified">
|
<N8nTooltip v-if="verified" placement="top">
|
||||||
<template #content>{{ i18n.baseText('communityNodeInfo.approved') }}</template>
|
<template #content>{{
|
||||||
|
official
|
||||||
|
? i18n.baseText('communityNodeInfo.officialApproved')
|
||||||
|
: i18n.baseText('communityNodeInfo.approved')
|
||||||
|
}}</template>
|
||||||
<div>
|
<div>
|
||||||
<FontAwesomeIcon :class="$style.tooltipIcon" icon="check-circle" />
|
<ShieldIcon :class="$style.tooltipIcon" />
|
||||||
<N8nText color="text-light" size="xsmall" bold data-test-id="verified-tag">
|
<N8nText color="text-light" size="xsmall" bold data-test-id="verified-tag">
|
||||||
{{ i18n.baseText('communityNodeInfo.approved.label') }}
|
{{ i18n.baseText('communityNodeInfo.approved.label') }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
</div>
|
</div>
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
|
|
||||||
<N8nTooltip placement="top" v-else>
|
<N8nTooltip v-else placement="top">
|
||||||
<template #content>{{ i18n.baseText('communityNodeInfo.unverified') }}</template>
|
<template #content>{{ i18n.baseText('communityNodeInfo.unverified') }}</template>
|
||||||
<div>
|
<div>
|
||||||
<FontAwesomeIcon :class="$style.tooltipIcon" icon="cube" />
|
<FontAwesomeIcon :class="$style.tooltipIcon" icon="cube" />
|
||||||
@@ -146,7 +153,7 @@ onMounted(async () => {
|
|||||||
<div style="padding-bottom: 8px">
|
<div style="padding-bottom: 8px">
|
||||||
{{ i18n.baseText('communityNodeInfo.contact.admin') }}
|
{{ i18n.baseText('communityNodeInfo.contact.admin') }}
|
||||||
</div>
|
</div>
|
||||||
<N8nText bold v-if="ownerEmailList.length">
|
<N8nText v-if="ownerEmailList.length" bold>
|
||||||
{{ ownerEmailList.join(', ') }}
|
{{ ownerEmailList.join(', ') }}
|
||||||
</N8nText>
|
</N8nText>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
@@ -188,12 +195,13 @@ onMounted(async () => {
|
|||||||
.info div {
|
.info div {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-3xs);
|
gap: var(--spacing-4xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltipIcon {
|
.tooltipIcon {
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
|
width: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contactOwnerHint {
|
.contactOwnerHint {
|
||||||
|
|||||||
@@ -120,11 +120,13 @@ registerKeyHook(`CategoryLeft_${props.category}`, {
|
|||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
</span>
|
</span>
|
||||||
</CategoryItem>
|
</CategoryItem>
|
||||||
|
|
||||||
<div v-if="expanded && actionCount > 0 && $slots.default" :class="$style.contentSlot">
|
<div v-if="expanded && actionCount > 0 && $slots.default" :class="$style.contentSlot">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CommunityNodeInstallHint
|
<CommunityNodeInstallHint
|
||||||
v-if="isPreview"
|
v-if="isPreview && expanded"
|
||||||
:hint="i18n.baseText('communityNodeItem.actions.hint')"
|
:hint="i18n.baseText('communityNodeItem.actions.hint')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export type CommunityNodeDetails = {
|
|||||||
description: string;
|
description: string;
|
||||||
packageName: string;
|
packageName: string;
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
|
official: boolean;
|
||||||
|
companyName?: string;
|
||||||
nodeIcon?: NodeIconSource;
|
nodeIcon?: NodeIconSource;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
mockNodeCreateElement,
|
mockNodeCreateElement,
|
||||||
mockSectionCreateElement,
|
mockSectionCreateElement,
|
||||||
} from './__tests__/utils';
|
} from './__tests__/utils';
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
|
||||||
vi.mock('@/stores/settings.store', () => ({
|
vi.mock('@/stores/settings.store', () => ({
|
||||||
useSettingsStore: vi.fn(() => ({ settings: {}, isAskAiEnabled: true })),
|
useSettingsStore: vi.fn(() => ({ settings: {}, isAskAiEnabled: true })),
|
||||||
@@ -134,6 +136,9 @@ describe('NodeCreator - utils', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('prepareCommunityNodeDetailsViewStack', () => {
|
describe('prepareCommunityNodeDetailsViewStack', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createTestingPinia());
|
||||||
|
});
|
||||||
const nodeCreateElement: NodeCreateElement = {
|
const nodeCreateElement: NodeCreateElement = {
|
||||||
key: 'n8n-nodes-preview-test.OtherNode',
|
key: 'n8n-nodes-preview-test.OtherNode',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -162,6 +167,7 @@ describe('NodeCreator - utils', () => {
|
|||||||
nodeIcon: undefined,
|
nodeIcon: undefined,
|
||||||
packageName: 'n8n-nodes-test',
|
packageName: 'n8n-nodes-test',
|
||||||
title: 'Other Node',
|
title: 'Other Node',
|
||||||
|
official: false,
|
||||||
},
|
},
|
||||||
hasSearch: false,
|
hasSearch: false,
|
||||||
items: [
|
items: [
|
||||||
@@ -185,7 +191,7 @@ describe('NodeCreator - utils', () => {
|
|||||||
mode: 'community-node',
|
mode: 'community-node',
|
||||||
rootView: undefined,
|
rootView: undefined,
|
||||||
subcategory: 'Other Node',
|
subcategory: 'Other Node',
|
||||||
title: 'Community node details',
|
title: 'Node details',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -255,6 +261,7 @@ describe('NodeCreator - utils', () => {
|
|||||||
nodeIcon: undefined,
|
nodeIcon: undefined,
|
||||||
packageName: 'n8n-nodes-test',
|
packageName: 'n8n-nodes-test',
|
||||||
title: 'Other Node',
|
title: 'Other Node',
|
||||||
|
official: false,
|
||||||
},
|
},
|
||||||
hasSearch: false,
|
hasSearch: false,
|
||||||
items: [
|
items: [
|
||||||
@@ -322,7 +329,7 @@ describe('NodeCreator - utils', () => {
|
|||||||
mode: 'actions',
|
mode: 'actions',
|
||||||
rootView: undefined,
|
rootView: undefined,
|
||||||
subcategory: 'Other Node',
|
subcategory: 'Other Node',
|
||||||
title: 'Community node details',
|
title: 'Node details',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { useSettingsStore } from '@/stores/settings.store';
|
|||||||
import { SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
|
import { SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
|
||||||
import type { NodeIconSource } from '../../../utils/nodeIcon';
|
import type { NodeIconSource } from '../../../utils/nodeIcon';
|
||||||
import type { CommunityNodeDetails, ViewStack } from './composables/useViewStacks';
|
import type { CommunityNodeDetails, ViewStack } from './composables/useViewStacks';
|
||||||
|
import { useNodeTypesStore } from '../../../stores/nodeTypes.store';
|
||||||
|
|
||||||
const COMMUNITY_NODE_TYPE_PREVIEW_TOKEN = '-preview';
|
const COMMUNITY_NODE_TYPE_PREVIEW_TOKEN = '-preview';
|
||||||
|
|
||||||
@@ -264,6 +265,8 @@ export function prepareCommunityNodeDetailsViewStack(
|
|||||||
): ViewStack {
|
): ViewStack {
|
||||||
const installed = !isNodePreviewKey(item.key);
|
const installed = !isNodePreviewKey(item.key);
|
||||||
const packageName = removePreviewToken(item.key.split('.')[0]);
|
const packageName = removePreviewToken(item.key.split('.')[0]);
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
const nodeType = nodeTypesStore.communityNodeType(removePreviewToken(item.key));
|
||||||
|
|
||||||
const communityNodeDetails: CommunityNodeDetails = {
|
const communityNodeDetails: CommunityNodeDetails = {
|
||||||
title: item.properties.displayName,
|
title: item.properties.displayName,
|
||||||
@@ -271,7 +274,9 @@ export function prepareCommunityNodeDetailsViewStack(
|
|||||||
key: item.key,
|
key: item.key,
|
||||||
nodeIcon,
|
nodeIcon,
|
||||||
installed,
|
installed,
|
||||||
|
official: nodeType?.isOfficialNode ?? false,
|
||||||
packageName,
|
packageName,
|
||||||
|
companyName: nodeType?.companyName,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (nodeActions.length) {
|
if (nodeActions.length) {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"generic.resetAllFilters": "Reset all filters",
|
"generic.resetAllFilters": "Reset all filters",
|
||||||
"generic.communityNode": "Community Node",
|
"generic.communityNode": "Community Node",
|
||||||
"generic.communityNode.tooltip": "This is a node from our community. It's part of the {packageName} package. <a href=\"{docURL}\" target=\"_blank\" title=\"Read the n8n docs\">Learn more</a>",
|
"generic.communityNode.tooltip": "This is a node from our community. It's part of the {packageName} package. <a href=\"{docURL}\" target=\"_blank\" title=\"Read the n8n docs\">Learn more</a>",
|
||||||
|
"generic.officialNode.tooltip": "This is an official node maintained by {author}",
|
||||||
"generic.copy": "Copy",
|
"generic.copy": "Copy",
|
||||||
"generic.delete": "Delete",
|
"generic.delete": "Delete",
|
||||||
"generic.dontShowAgain": "Don't show again",
|
"generic.dontShowAgain": "Don't show again",
|
||||||
@@ -1409,7 +1410,7 @@
|
|||||||
"nodeSettings.scopes.expandedNoticeWithScopes": "<a data-key=\"toggle-expand\">{count} scope</a> available for {activeCredential} credentials<br>{scopes}<br><a data-key=\"show-less\">Show less</a> | <a data-key=\"toggle-expand\">{count} scopes</a> available for {activeCredential} credentials<br>{scopes}<br><a data-key=\"show-less\">Show less</a>",
|
"nodeSettings.scopes.expandedNoticeWithScopes": "<a data-key=\"toggle-expand\">{count} scope</a> available for {activeCredential} credentials<br>{scopes}<br><a data-key=\"show-less\">Show less</a> | <a data-key=\"toggle-expand\">{count} scopes</a> available for {activeCredential} credentials<br>{scopes}<br><a data-key=\"show-less\">Show less</a>",
|
||||||
"nodeSettings.scopes.notice": "<a data-key=\"toggle-expand\">{count} scope</a> available for {activeCredential} credentials | <a data-key=\"toggle-expand\">{count} scopes</a> available for {activeCredential} credentials",
|
"nodeSettings.scopes.notice": "<a data-key=\"toggle-expand\">{count} scope</a> available for {activeCredential} credentials | <a data-key=\"toggle-expand\">{count} scopes</a> available for {activeCredential} credentials",
|
||||||
"nodeSettings.theNodeIsNotValidAsItsTypeIsUnknown": "The node is not valid as its type ({nodeType}) is unknown",
|
"nodeSettings.theNodeIsNotValidAsItsTypeIsUnknown": "The node is not valid as its type ({nodeType}) is unknown",
|
||||||
"nodeSettings.communityNodeDetails.title": "Community node details",
|
"nodeSettings.communityNodeDetails.title": "Node details",
|
||||||
"nodeSettings.communityNodeUnknown.title": "Install this node to use it",
|
"nodeSettings.communityNodeUnknown.title": "Install this node to use it",
|
||||||
"nodeSettings.communityNodeUnknown.description": "This node is not currently installed. It's part of the {action} community package.",
|
"nodeSettings.communityNodeUnknown.description": "This node is not currently installed. It's part of the {action} community package.",
|
||||||
"nodeSettings.communityNodeUnknown.installLink.text": "How to install community nodes",
|
"nodeSettings.communityNodeUnknown.installLink.text": "How to install community nodes",
|
||||||
@@ -3170,7 +3171,9 @@
|
|||||||
"communityNodeItem.actions.hint": "Install this node to start using actions",
|
"communityNodeItem.actions.hint": "Install this node to start using actions",
|
||||||
"communityNodeItem.label": "Add to workflow",
|
"communityNodeItem.label": "Add to workflow",
|
||||||
"communityNodeDetails.installed": "Installed",
|
"communityNodeDetails.installed": "Installed",
|
||||||
|
"communityNodeDetails.install": "Install node",
|
||||||
"communityNodeInfo.approved": "This community node has been reviewed and approved by n8n",
|
"communityNodeInfo.approved": "This community node has been reviewed and approved by n8n",
|
||||||
|
"communityNodeInfo.officialApproved": "This node has been reviewed and approved by n8n",
|
||||||
"communityNodeInfo.approved.label": "Verified",
|
"communityNodeInfo.approved.label": "Verified",
|
||||||
"communityNodeInfo.unverified": "This community node was added via npm and has not been verified by n8n",
|
"communityNodeInfo.unverified": "This community node was added via npm and has not been verified by n8n",
|
||||||
"communityNodeInfo.unverified.label": "Via npm",
|
"communityNodeInfo.unverified.label": "Via npm",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
ActionResultRequestDto,
|
ActionResultRequestDto,
|
||||||
|
CommunityNodeType,
|
||||||
OptionsRequestDto,
|
OptionsRequestDto,
|
||||||
ResourceLocatorRequestDto,
|
ResourceLocatorRequestDto,
|
||||||
ResourceMapperFieldsRequestDto,
|
ResourceMapperFieldsRequestDto,
|
||||||
@@ -35,7 +36,7 @@ export type NodeTypesStore = ReturnType<typeof useNodeTypesStore>;
|
|||||||
export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
||||||
const nodeTypes = ref<NodeTypesByTypeNameAndVersion>({});
|
const nodeTypes = ref<NodeTypesByTypeNameAndVersion>({});
|
||||||
|
|
||||||
const communityPreviews = ref<INodeTypeDescription[]>([]);
|
const vettedCommunityNodeTypes = ref<Map<string, CommunityNodeType>>(new Map());
|
||||||
|
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
|
|
||||||
@@ -47,34 +48,41 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
|||||||
// #region Computed
|
// #region Computed
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const communityNodeType = computed(() => {
|
||||||
|
return (nodeTypeName: string) => {
|
||||||
|
return vettedCommunityNodeTypes.value.get(nodeTypeName);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const officialCommunityNodeTypes = computed(() =>
|
||||||
|
Array.from(vettedCommunityNodeTypes.value.values())
|
||||||
|
.filter(({ isOfficialNode, isInstalled }) => isOfficialNode && !isInstalled)
|
||||||
|
.map(({ nodeDescription }) => nodeDescription),
|
||||||
|
);
|
||||||
|
|
||||||
|
const unofficialCommunityNodeTypes = computed(() =>
|
||||||
|
Array.from(vettedCommunityNodeTypes.value.values())
|
||||||
|
.filter(({ isOfficialNode, isInstalled }) => !isOfficialNode && !isInstalled)
|
||||||
|
.map(({ nodeDescription }) => nodeDescription),
|
||||||
|
);
|
||||||
|
|
||||||
const communityNodesAndActions = computed(() => {
|
const communityNodesAndActions = computed(() => {
|
||||||
return actionsGenerator.generateMergedNodesAndActions(communityPreviews.value, []);
|
return actionsGenerator.generateMergedNodesAndActions(unofficialCommunityNodeTypes.value, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
const allNodeTypes = computed(() => {
|
const allNodeTypes = computed(() => {
|
||||||
return Object.values(nodeTypes.value).reduce<INodeTypeDescription[]>(
|
return Object.values(nodeTypes.value).flatMap((nodeType) =>
|
||||||
(allNodeTypes, nodeType) => {
|
Object.keys(nodeType).map((version) => nodeType[Number(version)]),
|
||||||
const versionNumbers = Object.keys(nodeType).map(Number);
|
|
||||||
const allNodeVersions = versionNumbers.map((version) => nodeType[version]);
|
|
||||||
|
|
||||||
return [...allNodeTypes, ...allNodeVersions];
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const allLatestNodeTypes = computed(() => {
|
const allLatestNodeTypes = computed(() => {
|
||||||
return Object.values(nodeTypes.value).reduce<INodeTypeDescription[]>(
|
return Object.values(nodeTypes.value)
|
||||||
(allLatestNodeTypes, nodeVersions) => {
|
.map((nodeVersions) => {
|
||||||
const versionNumbers = Object.keys(nodeVersions).map(Number);
|
const versionNumbers = Object.keys(nodeVersions).map(Number);
|
||||||
const latestNodeVersion = nodeVersions[Math.max(...versionNumbers)];
|
return nodeVersions[Math.max(...versionNumbers)];
|
||||||
|
})
|
||||||
if (!latestNodeVersion) return allLatestNodeTypes;
|
.filter(Boolean);
|
||||||
|
|
||||||
return [...allLatestNodeTypes, latestNodeVersion];
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const getNodeType = computed(() => {
|
const getNodeType = computed(() => {
|
||||||
@@ -159,7 +167,9 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const visibleNodeTypes = computed(() => {
|
const visibleNodeTypes = computed(() => {
|
||||||
return allLatestNodeTypes.value.filter((nodeType: INodeTypeDescription) => !nodeType.hidden);
|
return allLatestNodeTypes.value
|
||||||
|
.concat(officialCommunityNodeTypes.value)
|
||||||
|
.filter((nodeType) => !nodeType.hidden);
|
||||||
});
|
});
|
||||||
|
|
||||||
const nativelyNumberSuffixedDefaults = computed(() => {
|
const nativelyNumberSuffixedDefaults = computed(() => {
|
||||||
@@ -360,11 +370,15 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
communityPreviews.value = await nodeTypesApi.fetchCommunityNodeTypes(
|
const communityNodeTypes = await nodeTypesApi.fetchCommunityNodeTypes(
|
||||||
rootStore.restApiContext,
|
rootStore.restApiContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
vettedCommunityNodeTypes.value = new Map(
|
||||||
|
communityNodeTypes.map((nodeType) => [nodeType.name, nodeType]),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
communityPreviews.value = [];
|
vettedCommunityNodeTypes.value = new Map();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -402,6 +416,7 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, () => {
|
|||||||
visibleNodeTypesByInputConnectionTypeNames,
|
visibleNodeTypesByInputConnectionTypeNames,
|
||||||
isConfigurableNode,
|
isConfigurableNode,
|
||||||
communityNodesAndActions,
|
communityNodesAndActions,
|
||||||
|
communityNodeType,
|
||||||
getResourceMapperFields,
|
getResourceMapperFields,
|
||||||
getLocalResourceMapperFields,
|
getLocalResourceMapperFields,
|
||||||
getNodeParameterActionResult,
|
getNodeParameterActionResult,
|
||||||
|
|||||||
Reference in New Issue
Block a user