diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts
index c8254038db609..5f159008ac1f5 100644
--- a/packages/editor-ui/src/Interface.ts
+++ b/packages/editor-ui/src/Interface.ts
@@ -1277,12 +1277,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 {
diff --git a/packages/editor-ui/src/components/CredentialIcon.vue b/packages/editor-ui/src/components/CredentialIcon.vue
index b20750e924dd7..bbe47e104d35b 100644
--- a/packages/editor-ui/src/components/CredentialIcon.vue
+++ b/packages/editor-ui/src/components/CredentialIcon.vue
@@ -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];
}
diff --git a/packages/editor-ui/src/components/__tests__/CredentialIcon.test.ts b/packages/editor-ui/src/components/__tests__/CredentialIcon.test.ts
new file mode 100644
index 0000000000000..3ebf73f93d0a8
--- /dev/null
+++ b/packages/editor-ui/src/components/__tests__/CredentialIcon.test.ts
@@ -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',
+ );
+ });
+});
diff --git a/packages/editor-ui/src/components/__tests__/testData/nodeTypesTestData.ts b/packages/editor-ui/src/components/__tests__/testData/nodeTypesTestData.ts
new file mode 100644
index 0000000000000..c5c07eb767a29
--- /dev/null
+++ b/packages/editor-ui/src/components/__tests__/testData/nodeTypesTestData.ts
@@ -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 here.',
+ },
+ {
+ 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 here.',
+ },
+ {
+ 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 expression.',
+ },
+ {
+ 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',
+};
diff --git a/packages/editor-ui/src/stores/credentials.store.ts b/packages/editor-ui/src/stores/credentials.store.ts
index c7aeaa5d269dd..0d024eb3e3fa6 100644
--- a/packages/editor-ui/src/stores/credentials.store.ts
+++ b/packages/editor-ui/src/stores/credentials.store.ts
@@ -130,9 +130,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;
}
diff --git a/packages/editor-ui/src/stores/nodeTypes.store.ts b/packages/editor-ui/src/stores/nodeTypes.store.ts
index 8323bd0907776..00456f546594e 100644
--- a/packages/editor-ui/src/stores/nodeTypes.store.ts
+++ b/packages/editor-ui/src/stores/nodeTypes.store.ts
@@ -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';
@@ -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;
export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
state: (): INodeTypesState => ({
@@ -191,36 +185,11 @@ export const useNodeTypesStore = defineStore(STORES.NODE_TYPES, {
},
actions: {
setNodeTypes(newNodeTypes: INodeTypeDescription[] = []): void {
- const nodeTypes = newNodeTypes.reduce>>(
- (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(
diff --git a/packages/editor-ui/src/utils/nodeTypes/nodeTypeTransforms.ts b/packages/editor-ui/src/utils/nodeTypes/nodeTypeTransforms.ts
new file mode 100644
index 0000000000000..a068f86c5949a
--- /dev/null
+++ b/packages/editor-ui/src/utils/nodeTypes/nodeTypeTransforms.ts
@@ -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((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;
+}