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:
Tomi Turtiainen 2023-11-28 15:14:22 +02:00 committed by GitHub
parent a37f1cb0ba
commit 4074107511
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 1115 additions and 47 deletions

View file

@ -1310,12 +1310,14 @@ export interface ISettingsState {
saveManualExecutions: boolean;
}
export interface INodeTypesState {
nodeTypes: {
[nodeType: string]: {
[version: number]: INodeTypeDescription;
};
export type NodeTypesByTypeNameAndVersion = {
[nodeType: string]: {
[version: number]: INodeTypeDescription;
};
};
export interface INodeTypesState {
nodeTypes: NodeTypesByTypeNameAndVersion;
}
export interface ITemplateState {

View file

@ -44,8 +44,11 @@ export default defineComponent({
const nodeType = this.credentialWithIcon.icon.replace('node:', '');
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) {
return nodesWithAccess[0];
}

View file

@ -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',
);
});
});

View file

@ -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',
};

View file

@ -132,9 +132,9 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
getNodesWithAccess() {
return (credentialTypeName: string) => {
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) {
return false;
}

View file

@ -6,12 +6,7 @@ import {
getResourceLocatorResults,
getResourceMapperFields,
} from '@/api/nodeTypes';
import {
DEFAULT_NODETYPE_VERSION,
HTTP_REQUEST_NODE_TYPE,
STORES,
CREDENTIAL_ONLY_HTTP_NODE_VERSION,
} from '@/constants';
import { HTTP_REQUEST_NODE_TYPE, STORES, CREDENTIAL_ONLY_HTTP_NODE_VERSION } from '@/constants';
import type { INodeTypesState, DynamicNodeParameters } from '@/Interface';
import { addHeaders, addNodeTranslation } from '@/plugins/i18n';
import { omit } from '@/utils/typesUtils';
@ -35,10 +30,9 @@ import {
getCredentialTypeName,
isCredentialOnlyNodeType,
} from '@/utils/credentialOnlyNodes';
import { groupNodeTypesByNameAndType } from '@/utils/nodeTypes/nodeTypeTransforms';
function getNodeVersions(nodeType: INodeTypeDescription) {
return Array.isArray(nodeType.version) ? nodeType.version : [nodeType.version];
}
export type NodeTypesStore = ReturnType<typeof useNodeTypesStore>;
export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
state: (): INodeTypesState => ({
@ -196,36 +190,11 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
},
actions: {
setNodeTypes(newNodeTypes: INodeTypeDescription[] = []): void {
const nodeTypes = newNodeTypes.reduce<Record<string, Record<string, INodeTypeDescription>>>(
(acc, newNodeType) => {
const newNodeVersions = getNodeVersions(newNodeType);
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;
const nodeTypes = groupNodeTypesByNameAndType(newNodeTypes);
this.nodeTypes = {
...this.nodeTypes,
...nodeTypes,
};
},
removeNodeTypes(nodeTypesToRemove: INodeTypeDescription[]): void {
this.nodeTypes = nodeTypesToRemove.reduce(

View 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;
}