mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -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;
|
||||
}
|
||||
|
||||
export interface INodeTypesState {
|
||||
nodeTypes: {
|
||||
export type NodeTypesByTypeNameAndVersion = {
|
||||
[nodeType: string]: {
|
||||
[version: number]: INodeTypeDescription;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export interface INodeTypesState {
|
||||
nodeTypes: NodeTypesByTypeNameAndVersion;
|
||||
}
|
||||
|
||||
export interface ITemplateState {
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
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