mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Fix credential icon for old node type version (#7843)
If a credential was for a node's older version, its icon was not shown.
This commit is contained in:
parent
a37f1cb0ba
commit
4074107511
|
@ -1310,12 +1310,14 @@ export interface ISettingsState {
|
||||||
saveManualExecutions: boolean;
|
saveManualExecutions: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodeTypesState {
|
export type NodeTypesByTypeNameAndVersion = {
|
||||||
nodeTypes: {
|
[nodeType: string]: {
|
||||||
[nodeType: string]: {
|
[version: number]: INodeTypeDescription;
|
||||||
[version: number]: INodeTypeDescription;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface INodeTypesState {
|
||||||
|
nodeTypes: NodeTypesByTypeNameAndVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITemplateState {
|
export interface ITemplateState {
|
||||||
|
|
|
@ -44,8 +44,11 @@ export default defineComponent({
|
||||||
const nodeType = this.credentialWithIcon.icon.replace('node:', '');
|
const nodeType = this.credentialWithIcon.icon.replace('node:', '');
|
||||||
return this.nodeTypesStore.getNodeType(nodeType);
|
return this.nodeTypesStore.getNodeType(nodeType);
|
||||||
}
|
}
|
||||||
const nodesWithAccess = this.credentialsStore.getNodesWithAccess(this.credentialTypeName);
|
if (!this.credentialTypeName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodesWithAccess = this.credentialsStore.getNodesWithAccess(this.credentialTypeName);
|
||||||
if (nodesWithAccess.length) {
|
if (nodesWithAccess.length) {
|
||||||
return nodesWithAccess[0];
|
return nodesWithAccess[0];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import CredentialIcon from '@/components/CredentialIcon.vue';
|
||||||
|
import { STORES } from '@/constants';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import * as testNodeTypes from './testData/nodeTypesTestData';
|
||||||
|
import merge from 'lodash-es/merge';
|
||||||
|
import { groupNodeTypesByNameAndType } from '@/utils/nodeTypes/nodeTypeTransforms';
|
||||||
|
|
||||||
|
const defaultState = {
|
||||||
|
[STORES.CREDENTIALS]: {},
|
||||||
|
[STORES.NODE_TYPES]: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(CredentialIcon, {
|
||||||
|
pinia: createTestingPinia({
|
||||||
|
initialState: defaultState,
|
||||||
|
}),
|
||||||
|
global: {
|
||||||
|
stubs: ['n8n-tooltip'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CredentialIcon', () => {
|
||||||
|
const findIcon = (baseElement: Element) => baseElement.querySelector('img');
|
||||||
|
|
||||||
|
it('shows correct icon for credential type that is for the latest node type version', () => {
|
||||||
|
const { baseElement } = renderComponent({
|
||||||
|
pinia: createTestingPinia({
|
||||||
|
initialState: merge(defaultState, {
|
||||||
|
[STORES.CREDENTIALS]: {},
|
||||||
|
[STORES.NODE_TYPES]: {
|
||||||
|
nodeTypes: groupNodeTypesByNameAndType([
|
||||||
|
testNodeTypes.twitterV1,
|
||||||
|
testNodeTypes.twitterV2,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
props: {
|
||||||
|
credentialTypeName: 'twitterOAuth2Api',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findIcon(baseElement)).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
'/icons/n8n-nodes-base/dist/nodes/Twitter/x.svg',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows correct icon for credential type that is for an older node type version', () => {
|
||||||
|
const { baseElement } = renderComponent({
|
||||||
|
pinia: createTestingPinia({
|
||||||
|
initialState: merge(defaultState, {
|
||||||
|
[STORES.CREDENTIALS]: {},
|
||||||
|
[STORES.NODE_TYPES]: {
|
||||||
|
nodeTypes: groupNodeTypesByNameAndType([
|
||||||
|
testNodeTypes.twitterV1,
|
||||||
|
testNodeTypes.twitterV2,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
props: {
|
||||||
|
credentialTypeName: 'twitterOAuth1Api',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(findIcon(baseElement)).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
'/icons/n8n-nodes-base/dist/nodes/Twitter/x.svg',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,964 @@
|
||||||
|
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const twitterV2: INodeTypeDescription = {
|
||||||
|
displayName: 'X (Formerly Twitter)',
|
||||||
|
name: 'n8n-nodes-base.twitter',
|
||||||
|
group: ['output'],
|
||||||
|
subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}',
|
||||||
|
description: 'Post, like, and search tweets, send messages, search users, and add users to lists',
|
||||||
|
defaultVersion: 2,
|
||||||
|
version: 2,
|
||||||
|
defaults: { name: 'X' },
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [{ name: 'twitterOAuth2Api', required: true }],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Resource',
|
||||||
|
name: 'resource',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Direct Message',
|
||||||
|
value: 'directMessage',
|
||||||
|
description: 'Send a direct message to a user',
|
||||||
|
},
|
||||||
|
{ name: 'List', value: 'list', description: 'Add a user to a list' },
|
||||||
|
{ name: 'Tweet', value: 'tweet', description: 'Create, like, search, or delete a tweet' },
|
||||||
|
{ name: 'User', value: 'user', description: 'Search users by username' },
|
||||||
|
{ name: 'Custom API Call', value: '__CUSTOM_API_CALL__' },
|
||||||
|
],
|
||||||
|
default: 'tweet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['directMessage'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
description: 'Send a direct message to a user',
|
||||||
|
action: 'Create Direct Message',
|
||||||
|
},
|
||||||
|
{ name: 'Custom API Call', value: '__CUSTOM_API_CALL__' },
|
||||||
|
],
|
||||||
|
default: 'create',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'User',
|
||||||
|
name: 'user',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'username', value: '' },
|
||||||
|
required: true,
|
||||||
|
description: 'The user you want to send the message to',
|
||||||
|
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'By Username',
|
||||||
|
name: 'username',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. n8n',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. 1068479892537384960',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Text',
|
||||||
|
name: 'text',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
typeOptions: { rows: 2 },
|
||||||
|
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||||
|
description:
|
||||||
|
'The text of the direct message. URL encoding is required. Max length of 10,000 characters.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Attachment ID',
|
||||||
|
name: 'attachments',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: '1664279886239010824',
|
||||||
|
description: 'The attachment ID to associate with the message',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['list'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Add Member',
|
||||||
|
value: 'add',
|
||||||
|
description: 'Add a member to a list',
|
||||||
|
action: 'Add Member to List',
|
||||||
|
},
|
||||||
|
{ name: 'Custom API Call', value: '__CUSTOM_API_CALL__' },
|
||||||
|
],
|
||||||
|
default: 'add',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'List',
|
||||||
|
name: 'list',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'id', value: '' },
|
||||||
|
required: true,
|
||||||
|
description: 'The list you want to add the user to',
|
||||||
|
displayOptions: { show: { operation: ['add'], resource: ['list'] } },
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. 99923132',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. https://twitter.com/i/lists/99923132',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'User',
|
||||||
|
name: 'user',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'username', value: '' },
|
||||||
|
required: true,
|
||||||
|
description: 'The user you want to add to the list',
|
||||||
|
displayOptions: { show: { operation: ['add'], resource: ['list'] } },
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'By Username',
|
||||||
|
name: 'username',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. n8n',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. 1068479892537384960',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['tweet'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
description: 'Create, quote, or reply to a tweet',
|
||||||
|
action: 'Create Tweet',
|
||||||
|
},
|
||||||
|
{ name: 'Delete', value: 'delete', description: 'Delete a tweet', action: 'Delete Tweet' },
|
||||||
|
{ name: 'Like', value: 'like', description: 'Like a tweet', action: 'Like Tweet' },
|
||||||
|
{
|
||||||
|
name: 'Retweet',
|
||||||
|
value: 'retweet',
|
||||||
|
description: 'Retweet a tweet',
|
||||||
|
action: 'Retweet Tweet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Search',
|
||||||
|
value: 'search',
|
||||||
|
description: 'Search for tweets from the last seven days',
|
||||||
|
action: 'Search Tweets',
|
||||||
|
},
|
||||||
|
{ name: 'Custom API Call', value: '__CUSTOM_API_CALL__' },
|
||||||
|
],
|
||||||
|
default: 'create',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Text',
|
||||||
|
name: 'text',
|
||||||
|
type: 'string',
|
||||||
|
typeOptions: { rows: 2 },
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
displayOptions: { show: { operation: ['create'], resource: ['tweet'] } },
|
||||||
|
description:
|
||||||
|
'The text of the status update. URLs must be encoded. Links wrapped with the t.co shortener will affect character count',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: { show: { operation: ['create'], resource: ['tweet'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Location ID',
|
||||||
|
name: 'location',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: '4e696bef7e24d378',
|
||||||
|
default: '',
|
||||||
|
description: 'Location information for the tweet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Media ID',
|
||||||
|
name: 'attachments',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: '1664279886239010824',
|
||||||
|
description: 'The attachment ID to associate with the message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Quote a Tweet',
|
||||||
|
name: 'inQuoteToStatusId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'id', value: '' },
|
||||||
|
description: 'The tweet being quoted',
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. 1187836157394112513',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Reply to Tweet',
|
||||||
|
name: 'inReplyToStatusId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'id', value: '' },
|
||||||
|
description: 'The tweet being replied to',
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. 1187836157394112513',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Locations are not supported due to Twitter V2 API limitations',
|
||||||
|
name: 'noticeLocation',
|
||||||
|
type: 'notice',
|
||||||
|
displayOptions: { show: { '/additionalFields.location': [''] } },
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Attachements are not supported due to Twitter V2 API limitations',
|
||||||
|
name: 'noticeAttachments',
|
||||||
|
type: 'notice',
|
||||||
|
displayOptions: { show: { '/additionalFields.attachments': [''] } },
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Tweet',
|
||||||
|
name: 'tweetDeleteId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'id', value: '' },
|
||||||
|
required: true,
|
||||||
|
description: 'The tweet to delete',
|
||||||
|
displayOptions: { show: { resource: ['tweet'], operation: ['delete'] } },
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. 1187836157394112513',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Tweet',
|
||||||
|
name: 'tweetId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'id', value: '' },
|
||||||
|
required: true,
|
||||||
|
description: 'The tweet to like',
|
||||||
|
displayOptions: { show: { operation: ['like'], resource: ['tweet'] } },
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. 1187836157394112513',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Search Term',
|
||||||
|
name: 'searchText',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g. automation',
|
||||||
|
displayOptions: { show: { operation: ['search'], resource: ['tweet'] } },
|
||||||
|
description:
|
||||||
|
'A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity. Check the searching examples <a href="https://developer.twitter.com/en/docs/tweets/search/guides/standard-operators">here</a>.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to return all results or only up to a given limit',
|
||||||
|
displayOptions: { show: { resource: ['tweet'], operation: ['search'] } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
default: 50,
|
||||||
|
description: 'Max number of results to return',
|
||||||
|
typeOptions: { minValue: 1 },
|
||||||
|
displayOptions: { show: { resource: ['tweet'], operation: ['search'], returnAll: [false] } },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: { show: { operation: ['search'], resource: ['tweet'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Sort Order',
|
||||||
|
name: 'sortOrder',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{ name: 'Recent', value: 'recency' },
|
||||||
|
{ name: 'Relevant', value: 'relevancy' },
|
||||||
|
],
|
||||||
|
description: 'The order in which to return results',
|
||||||
|
default: 'recency',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'After',
|
||||||
|
name: 'startTime',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
description:
|
||||||
|
"Tweets before this date will not be returned. This date must be within the last 7 days if you don't have Academic Research access.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Before',
|
||||||
|
name: 'endTime',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
description:
|
||||||
|
"Tweets after this date will not be returned. This date must be within the last 7 days if you don't have Academic Research access.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Tweet Fields',
|
||||||
|
name: 'tweetFieldsObject',
|
||||||
|
type: 'multiOptions',
|
||||||
|
options: [
|
||||||
|
{ name: 'Attachments', value: 'attachments' },
|
||||||
|
{ name: 'Author ID', value: 'author_id' },
|
||||||
|
{ name: 'Context Annotations', value: 'context_annotations' },
|
||||||
|
{ name: 'Conversation ID', value: 'conversation_id' },
|
||||||
|
{ name: 'Created At', value: 'created_at' },
|
||||||
|
{ name: 'Edit Controls', value: 'edit_controls' },
|
||||||
|
{ name: 'Entities', value: 'entities' },
|
||||||
|
{ name: 'Geo', value: 'geo' },
|
||||||
|
{ name: 'ID', value: 'id' },
|
||||||
|
{ name: 'In Reply To User ID', value: 'in_reply_to_user_id' },
|
||||||
|
{ name: 'Lang', value: 'lang' },
|
||||||
|
{ name: 'Non Public Metrics', value: 'non_public_metrics' },
|
||||||
|
{ name: 'Public Metrics', value: 'public_metrics' },
|
||||||
|
{ name: 'Organic Metrics', value: 'organic_metrics' },
|
||||||
|
{ name: 'Promoted Metrics', value: 'promoted_metrics' },
|
||||||
|
{ name: 'Possibly Sensitive', value: 'possibly_sensitive' },
|
||||||
|
{ name: 'Referenced Tweets', value: 'referenced_tweets' },
|
||||||
|
{ name: 'Reply Settings', value: 'reply_settings' },
|
||||||
|
{ name: 'Source', value: 'source' },
|
||||||
|
{ name: 'Text', value: 'text' },
|
||||||
|
{ name: 'Withheld', value: 'withheld' },
|
||||||
|
],
|
||||||
|
default: [],
|
||||||
|
description:
|
||||||
|
'The fields to add to each returned tweet object. Default fields are: ID, text, edit_history_tweet_ids.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Tweet',
|
||||||
|
name: 'tweetId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'id', value: '' },
|
||||||
|
required: true,
|
||||||
|
description: 'The tweet to retweet',
|
||||||
|
displayOptions: { show: { operation: ['retweet'], resource: ['tweet'] } },
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. 1187836157394112513',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. https://twitter.com/n8n_io/status/1187836157394112513',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['user'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Get',
|
||||||
|
value: 'searchUser',
|
||||||
|
description: 'Retrieve a user by username',
|
||||||
|
action: 'Get User',
|
||||||
|
},
|
||||||
|
{ name: 'Custom API Call', value: '__CUSTOM_API_CALL__' },
|
||||||
|
],
|
||||||
|
default: 'searchUser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'User',
|
||||||
|
name: 'user',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: { mode: 'username', value: '' },
|
||||||
|
required: true,
|
||||||
|
description: 'The user you want to search',
|
||||||
|
displayOptions: {
|
||||||
|
show: { operation: ['searchUser'], resource: ['user'] },
|
||||||
|
hide: { me: [true] },
|
||||||
|
},
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'By Username',
|
||||||
|
name: 'username',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. n8n',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
validation: [],
|
||||||
|
placeholder: 'e.g. 1068479892537384960',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Me',
|
||||||
|
name: 'me',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: { show: { operation: ['searchUser'], resource: ['user'] } },
|
||||||
|
default: false,
|
||||||
|
description: 'Whether you want to search the authenticated user',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
iconUrl: 'icons/n8n-nodes-base/dist/nodes/Twitter/x.svg',
|
||||||
|
codex: {
|
||||||
|
categories: ['Marketing & Content'],
|
||||||
|
resources: {
|
||||||
|
primaryDocumentation: [
|
||||||
|
{ url: 'https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.twitter/' },
|
||||||
|
],
|
||||||
|
credentialDocumentation: [{ url: 'https://docs.n8n.io/credentials/twitter' }],
|
||||||
|
},
|
||||||
|
alias: ['Tweet', 'Twitter', 'X', 'X API'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const twitterV1: INodeTypeDescription = {
|
||||||
|
displayName: 'X (Formerly Twitter)',
|
||||||
|
name: 'n8n-nodes-base.twitter',
|
||||||
|
group: ['output'],
|
||||||
|
subtitle: '={{$parameter["operation"] + ":" + $parameter["resource"]}}',
|
||||||
|
description: 'Consume Twitter API',
|
||||||
|
defaultVersion: 2,
|
||||||
|
version: 1,
|
||||||
|
defaults: { name: 'Twitter' },
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [{ name: 'twitterOAuth1Api', required: true }],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Resource',
|
||||||
|
name: 'resource',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
options: [
|
||||||
|
{ name: 'Direct Message', value: 'directMessage' },
|
||||||
|
{ name: 'Tweet', value: 'tweet' },
|
||||||
|
],
|
||||||
|
default: 'tweet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['directMessage'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
description: 'Create a direct message',
|
||||||
|
action: 'Create a direct message',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'create',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'User ID',
|
||||||
|
name: 'userId',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||||
|
description: 'The ID of the user who should receive the direct message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Text',
|
||||||
|
name: 'text',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||||
|
description:
|
||||||
|
'The text of your Direct Message. URL encode as necessary. Max length of 10,000 characters.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: { show: { operation: ['create'], resource: ['directMessage'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Attachment',
|
||||||
|
name: 'attachment',
|
||||||
|
type: 'string',
|
||||||
|
default: 'data',
|
||||||
|
description:
|
||||||
|
'Name of the binary property which contain data that should be added to the direct message as attachment',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
displayOptions: { show: { resource: ['tweet'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Create',
|
||||||
|
value: 'create',
|
||||||
|
description: 'Create or reply a tweet',
|
||||||
|
action: 'Create a tweet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delete',
|
||||||
|
value: 'delete',
|
||||||
|
description: 'Delete a tweet',
|
||||||
|
action: 'Delete a tweet',
|
||||||
|
},
|
||||||
|
{ name: 'Like', value: 'like', description: 'Like a tweet', action: 'Like a tweet' },
|
||||||
|
{
|
||||||
|
name: 'Retweet',
|
||||||
|
value: 'retweet',
|
||||||
|
description: 'Retweet a tweet',
|
||||||
|
action: 'Retweet a tweet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Search',
|
||||||
|
value: 'search',
|
||||||
|
description: 'Search tweets',
|
||||||
|
action: 'Search for tweets',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'create',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Text',
|
||||||
|
name: 'text',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
displayOptions: { show: { operation: ['create'], resource: ['tweet'] } },
|
||||||
|
description:
|
||||||
|
'The text of the status update. URL encode as necessary. t.co link wrapping will affect character counts.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: { show: { operation: ['create'], resource: ['tweet'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Attachments',
|
||||||
|
name: 'attachments',
|
||||||
|
type: 'string',
|
||||||
|
default: 'data',
|
||||||
|
description:
|
||||||
|
'Name of the binary properties which contain data which should be added to tweet as attachment. Multiple ones can be comma-separated.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Display Coordinates',
|
||||||
|
name: 'displayCoordinates',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether or not to put a pin on the exact coordinates a Tweet has been sent from',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'In Reply to Tweet',
|
||||||
|
name: 'inReplyToStatusId',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The ID of an existing status that the update is in reply to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Location',
|
||||||
|
name: 'locationFieldsUi',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
placeholder: 'Add Location',
|
||||||
|
default: {},
|
||||||
|
description: 'Subscriber location information.n',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'locationFieldsValues',
|
||||||
|
displayName: 'Location',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Latitude',
|
||||||
|
name: 'latitude',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: 'The location latitude',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Longitude',
|
||||||
|
name: 'longitude',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: 'The location longitude',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Possibly Sensitive',
|
||||||
|
name: 'possiblySensitive',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether you are uploading Tweet media that might be considered sensitive content such as nudity, or medical procedures',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Tweet ID',
|
||||||
|
name: 'tweetId',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
displayOptions: { show: { operation: ['delete'], resource: ['tweet'] } },
|
||||||
|
description: 'The ID of the tweet to delete',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Search Text',
|
||||||
|
name: 'searchText',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
displayOptions: { show: { operation: ['search'], resource: ['tweet'] } },
|
||||||
|
description:
|
||||||
|
'A UTF-8, URL-encoded search query of 500 characters maximum, including operators. Queries may additionally be limited by complexity. Check the searching examples <a href="https://developer.twitter.com/en/docs/tweets/search/guides/standard-operators">here</a>.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Return All',
|
||||||
|
name: 'returnAll',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: { show: { operation: ['search'], resource: ['tweet'] } },
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to return all results or only up to a given limit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Limit',
|
||||||
|
name: 'limit',
|
||||||
|
type: 'number',
|
||||||
|
displayOptions: { show: { operation: ['search'], resource: ['tweet'], returnAll: [false] } },
|
||||||
|
typeOptions: { minValue: 1 },
|
||||||
|
default: 50,
|
||||||
|
description: 'Max number of results to return',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: { show: { operation: ['search'], resource: ['tweet'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Include Entities',
|
||||||
|
name: 'includeEntities',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether the entities node will be included',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Language Name or ID',
|
||||||
|
name: 'lang',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: { loadOptionsMethod: 'getLanguages' },
|
||||||
|
default: '',
|
||||||
|
description:
|
||||||
|
'Restricts tweets to the given language, given by an ISO 639-1 code. Language detection is best-effort. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Location',
|
||||||
|
name: 'locationFieldsUi',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
placeholder: 'Add Location',
|
||||||
|
default: {},
|
||||||
|
description: 'Subscriber location information.n',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'locationFieldsValues',
|
||||||
|
displayName: 'Location',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Latitude',
|
||||||
|
name: 'latitude',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: 'The location latitude',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Longitude',
|
||||||
|
name: 'longitude',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
description: 'The location longitude',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Radius',
|
||||||
|
name: 'radius',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{ name: 'Milles', value: 'mi' },
|
||||||
|
{ name: 'Kilometers', value: 'km' },
|
||||||
|
],
|
||||||
|
required: true,
|
||||||
|
description:
|
||||||
|
'Returns tweets by users located within a given radius of the given latitude/longitude',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Distance',
|
||||||
|
name: 'distance',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: { minValue: 0 },
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Result Type',
|
||||||
|
name: 'resultType',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Mixed',
|
||||||
|
value: 'mixed',
|
||||||
|
description: 'Include both popular and real time results in the response',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Recent',
|
||||||
|
value: 'recent',
|
||||||
|
description: 'Return only the most recent results in the response',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Popular',
|
||||||
|
value: 'popular',
|
||||||
|
description: 'Return only the most popular results in the response',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'mixed',
|
||||||
|
description: 'Specifies what type of search results you would prefer to receive',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Tweet Mode',
|
||||||
|
name: 'tweetMode',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{ name: 'Compatibility', value: 'compat' },
|
||||||
|
{ name: 'Extended', value: 'extended' },
|
||||||
|
],
|
||||||
|
default: 'compat',
|
||||||
|
description:
|
||||||
|
'When the extended mode is selected, the response contains the entire untruncated text of the Tweet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Until',
|
||||||
|
name: 'until',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: '',
|
||||||
|
description: 'Returns tweets created before the given date',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Tweet ID',
|
||||||
|
name: 'tweetId',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
displayOptions: { show: { operation: ['like'], resource: ['tweet'] } },
|
||||||
|
description: 'The ID of the tweet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: { show: { operation: ['like'], resource: ['tweet'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Include Entities',
|
||||||
|
name: 'includeEntities',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether the entities will be omitted',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Tweet ID',
|
||||||
|
name: 'tweetId',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
displayOptions: { show: { operation: ['retweet'], resource: ['tweet'] } },
|
||||||
|
description: 'The ID of the tweet',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Additional Fields',
|
||||||
|
name: 'additionalFields',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Field',
|
||||||
|
default: {},
|
||||||
|
displayOptions: { show: { operation: ['retweet'], resource: ['tweet'] } },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Trim User',
|
||||||
|
name: 'trimUser',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether each tweet returned in a timeline will include a user object including only the status authors numerical ID',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
iconUrl: 'icons/n8n-nodes-base/dist/nodes/Twitter/x.svg',
|
||||||
|
};
|
|
@ -132,9 +132,9 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
|
||||||
getNodesWithAccess() {
|
getNodesWithAccess() {
|
||||||
return (credentialTypeName: string) => {
|
return (credentialTypeName: string) => {
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const allLatestNodeTypes: INodeTypeDescription[] = nodeTypesStore.allLatestNodeTypes;
|
const allNodeTypes: INodeTypeDescription[] = nodeTypesStore.allNodeTypes;
|
||||||
|
|
||||||
return allLatestNodeTypes.filter((nodeType: INodeTypeDescription) => {
|
return allNodeTypes.filter((nodeType: INodeTypeDescription) => {
|
||||||
if (!nodeType.credentials) {
|
if (!nodeType.credentials) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,12 +6,7 @@ import {
|
||||||
getResourceLocatorResults,
|
getResourceLocatorResults,
|
||||||
getResourceMapperFields,
|
getResourceMapperFields,
|
||||||
} from '@/api/nodeTypes';
|
} from '@/api/nodeTypes';
|
||||||
import {
|
import { HTTP_REQUEST_NODE_TYPE, STORES, CREDENTIAL_ONLY_HTTP_NODE_VERSION } from '@/constants';
|
||||||
DEFAULT_NODETYPE_VERSION,
|
|
||||||
HTTP_REQUEST_NODE_TYPE,
|
|
||||||
STORES,
|
|
||||||
CREDENTIAL_ONLY_HTTP_NODE_VERSION,
|
|
||||||
} from '@/constants';
|
|
||||||
import type { INodeTypesState, DynamicNodeParameters } from '@/Interface';
|
import type { INodeTypesState, DynamicNodeParameters } from '@/Interface';
|
||||||
import { addHeaders, addNodeTranslation } from '@/plugins/i18n';
|
import { addHeaders, addNodeTranslation } from '@/plugins/i18n';
|
||||||
import { omit } from '@/utils/typesUtils';
|
import { omit } from '@/utils/typesUtils';
|
||||||
|
@ -35,10 +30,9 @@ import {
|
||||||
getCredentialTypeName,
|
getCredentialTypeName,
|
||||||
isCredentialOnlyNodeType,
|
isCredentialOnlyNodeType,
|
||||||
} from '@/utils/credentialOnlyNodes';
|
} from '@/utils/credentialOnlyNodes';
|
||||||
|
import { groupNodeTypesByNameAndType } from '@/utils/nodeTypes/nodeTypeTransforms';
|
||||||
|
|
||||||
function getNodeVersions(nodeType: INodeTypeDescription) {
|
export type NodeTypesStore = ReturnType<typeof useNodeTypesStore>;
|
||||||
return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
|
export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
|
||||||
state: (): INodeTypesState => ({
|
state: (): INodeTypesState => ({
|
||||||
|
@ -196,36 +190,11 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setNodeTypes(newNodeTypes: INodeTypeDescription[] = []): void {
|
setNodeTypes(newNodeTypes: INodeTypeDescription[] = []): void {
|
||||||
const nodeTypes = newNodeTypes.reduce<Record<string, Record<string, INodeTypeDescription>>>(
|
const nodeTypes = groupNodeTypesByNameAndType(newNodeTypes);
|
||||||
(acc, newNodeType) => {
|
this.nodeTypes = {
|
||||||
const newNodeVersions = getNodeVersions(newNodeType);
|
...this.nodeTypes,
|
||||||
|
...nodeTypes,
|
||||||
if (newNodeVersions.length === 0) {
|
};
|
||||||
const singleVersion = { [DEFAULT_NODETYPE_VERSION]: newNodeType };
|
|
||||||
|
|
||||||
acc[newNodeType.name] = singleVersion;
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const version of newNodeVersions) {
|
|
||||||
// Node exists with the same name
|
|
||||||
if (acc[newNodeType.name]) {
|
|
||||||
acc[newNodeType.name][version] = Object.assign(
|
|
||||||
acc[newNodeType.name][version] ?? {},
|
|
||||||
newNodeType,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
acc[newNodeType.name] = Object.assign(acc[newNodeType.name] ?? {}, {
|
|
||||||
[version]: newNodeType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ ...this.nodeTypes },
|
|
||||||
);
|
|
||||||
this.nodeTypes = nodeTypes;
|
|
||||||
},
|
},
|
||||||
removeNodeTypes(nodeTypesToRemove: INodeTypeDescription[]): void {
|
removeNodeTypes(nodeTypesToRemove: INodeTypeDescription[]): void {
|
||||||
this.nodeTypes = nodeTypesToRemove.reduce(
|
this.nodeTypes = nodeTypesToRemove.reduce(
|
||||||
|
|
57
packages/editor-ui/src/utils/nodeTypes/nodeTypeTransforms.ts
Normal file
57
packages/editor-ui/src/utils/nodeTypes/nodeTypeTransforms.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
import type { NodeTypesByTypeNameAndVersion } from '@/Interface';
|
||||||
|
import { DEFAULT_NODETYPE_VERSION } from '@/constants';
|
||||||
|
|
||||||
|
export function getNodeVersions(nodeType: INodeTypeDescription) {
|
||||||
|
return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups given node types by their name and version
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const nodeTypes = [
|
||||||
|
* { name: 'twitter', version: '1', ... },
|
||||||
|
* { name: 'twitter', version: '2', ... },
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* const groupedNodeTypes = groupNodeTypesByNameAndType(nodeTypes);
|
||||||
|
* // {
|
||||||
|
* // twitter: {
|
||||||
|
* // 1: { name: 'twitter', version: '1', ... },
|
||||||
|
* // 2: { name: 'twitter', version: '2', ... },
|
||||||
|
* // }
|
||||||
|
* // }
|
||||||
|
*/
|
||||||
|
export function groupNodeTypesByNameAndType(
|
||||||
|
nodeTypes: INodeTypeDescription[],
|
||||||
|
): NodeTypesByTypeNameAndVersion {
|
||||||
|
const groupedNodeTypes = nodeTypes.reduce<NodeTypesByTypeNameAndVersion>((groups, nodeType) => {
|
||||||
|
const newNodeVersions = getNodeVersions(nodeType);
|
||||||
|
|
||||||
|
if (newNodeVersions.length === 0) {
|
||||||
|
const singleVersion = { [DEFAULT_NODETYPE_VERSION]: nodeType };
|
||||||
|
|
||||||
|
groups[nodeType.name] = singleVersion;
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const version of newNodeVersions) {
|
||||||
|
// Node exists with the same name
|
||||||
|
if (groups[nodeType.name]) {
|
||||||
|
groups[nodeType.name][version] = Object.assign(
|
||||||
|
groups[nodeType.name][version] ?? {},
|
||||||
|
nodeType,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
groups[nodeType.name] = Object.assign(groups[nodeType.name] ?? {}, {
|
||||||
|
[version]: nodeType,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return groupedNodeTypes;
|
||||||
|
}
|
Loading…
Reference in a new issue