mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-02 07:01:30 -08:00
feat(Discord Node): New sendAndWait operation (#12894)
Co-authored-by: Dana <152518854+dana-gill@users.noreply.github.com>
This commit is contained in:
parent
066908060f
commit
d47bfddd65
|
@ -2,7 +2,10 @@
|
|||
"node": "n8n-nodes-base.discord",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Communication"],
|
||||
"categories": ["Communication", "HITL"],
|
||||
"subcategories": {
|
||||
"HITL": ["Human in the Loop"]
|
||||
},
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
|
@ -14,5 +17,6 @@
|
|||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.discord/"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"alias": ["human", "form", "wait", "hitl", "approval"]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import type { MockProxy } from 'jest-mock-extended';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import { SEND_AND_WAIT_OPERATION, type IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
import { versionDescription } from '../../../../v2/actions/versionDescription';
|
||||
import { DiscordV2 } from '../../../../v2/DiscordV2.node';
|
||||
import * as transport from '../../../../v2/transport/discord.api';
|
||||
|
||||
jest.mock('../../../../v2/transport/discord.api', () => {
|
||||
const originalModule = jest.requireActual('../../../../v2/transport/discord.api');
|
||||
return {
|
||||
...originalModule,
|
||||
discordApiRequest: jest.fn(async function (method: string) {
|
||||
if (method === 'POST') {
|
||||
return {};
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Test DiscordV2, message => sendAndWait', () => {
|
||||
let discord: DiscordV2;
|
||||
let mockExecuteFunctions: MockProxy<IExecuteFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
discord = new DiscordV2(versionDescription);
|
||||
mockExecuteFunctions = mock<IExecuteFunctions>();
|
||||
mockExecuteFunctions.helpers = {
|
||||
constructExecutionMetaData: jest.fn(() => []),
|
||||
returnJsonArray: jest.fn(() => []),
|
||||
} as any;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should send message and put execution to wait', async () => {
|
||||
const items = [{ json: { data: 'test' } }];
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((key: string) => {
|
||||
if (key === 'operation') return SEND_AND_WAIT_OPERATION;
|
||||
if (key === 'resource') return 'message';
|
||||
if (key === 'authentication') return 'botToken';
|
||||
if (key === 'message') return 'my message';
|
||||
if (key === 'subject') return '';
|
||||
if (key === 'approvalOptions.values') return {};
|
||||
if (key === 'responseType') return 'approval';
|
||||
if (key === 'sendTo') return 'channel';
|
||||
if (key === 'channelId') return 'channelID';
|
||||
if (key === 'options.limitWaitTime.values') return {};
|
||||
});
|
||||
|
||||
mockExecuteFunctions.putExecutionToWait.mockImplementation();
|
||||
mockExecuteFunctions.getInputData.mockReturnValue(items);
|
||||
mockExecuteFunctions.getInstanceId.mockReturnValue('instanceId');
|
||||
|
||||
mockExecuteFunctions.evaluateExpression.mockReturnValueOnce('http://localhost/waiting-webhook');
|
||||
mockExecuteFunctions.evaluateExpression.mockReturnValueOnce('nodeID');
|
||||
|
||||
const result = await discord.execute.call(mockExecuteFunctions);
|
||||
|
||||
expect(result).toEqual([items]);
|
||||
expect(transport.discordApiRequest).toHaveBeenCalledTimes(1);
|
||||
expect(mockExecuteFunctions.putExecutionToWait).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(transport.discordApiRequest).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'/channels/channelID/messages',
|
||||
{
|
||||
components: [
|
||||
{
|
||||
components: [
|
||||
{
|
||||
label: 'Approve',
|
||||
style: 5,
|
||||
type: 2,
|
||||
url: 'http://localhost/waiting-webhook/nodeID?approved=true',
|
||||
},
|
||||
],
|
||||
type: 1,
|
||||
},
|
||||
],
|
||||
embeds: [
|
||||
{
|
||||
color: 5814783,
|
||||
description:
|
||||
'my message\n\n_This message was sent automatically with _[n8n](https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=n8n-nodes-base.telegram_instanceId)',
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -9,6 +9,7 @@ import type {
|
|||
import { router } from './actions/router';
|
||||
import { versionDescription } from './actions/versionDescription';
|
||||
import { listSearch, loadOptions } from './methods';
|
||||
import { sendAndWaitWebhook } from '../../../utils/sendAndWait/utils';
|
||||
|
||||
export class DiscordV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
@ -25,6 +26,8 @@ export class DiscordV2 implements INodeType {
|
|||
loadOptions,
|
||||
};
|
||||
|
||||
webhook = sendAndWaitWebhook;
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
return await router.call(this);
|
||||
}
|
||||
|
|
|
@ -463,3 +463,40 @@ export const filesFixedCollection: INodeProperties = {
|
|||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const sendToProperties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Send To',
|
||||
name: 'sendTo',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
{
|
||||
name: 'Channel',
|
||||
value: 'channel',
|
||||
},
|
||||
],
|
||||
default: 'channel',
|
||||
description: 'Send message to a channel or DM to a user',
|
||||
},
|
||||
|
||||
{
|
||||
...userRLC,
|
||||
displayOptions: {
|
||||
show: {
|
||||
sendTo: ['user'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...textChannelRLC,
|
||||
displayOptions: {
|
||||
show: {
|
||||
sendTo: ['channel'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import type { INodeProperties } from 'n8n-workflow';
|
||||
import { SEND_AND_WAIT_OPERATION, type INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import * as deleteMessage from './deleteMessage.operation';
|
||||
import * as get from './get.operation';
|
||||
import * as getAll from './getAll.operation';
|
||||
import * as react from './react.operation';
|
||||
import * as send from './send.operation';
|
||||
import * as sendAndWait from './sendAndWait.operation';
|
||||
import { guildRLC } from '../common.description';
|
||||
|
||||
export { getAll, react, send, deleteMessage, get };
|
||||
export { getAll, react, send, deleteMessage, get, sendAndWait };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
|
@ -52,6 +53,12 @@ export const description: INodeProperties[] = [
|
|||
description: 'Send a message to a channel, thread, or member',
|
||||
action: 'Send a message',
|
||||
},
|
||||
{
|
||||
name: 'Send and Wait for Response',
|
||||
value: SEND_AND_WAIT_OPERATION,
|
||||
description: 'Send a message and wait for response',
|
||||
action: 'Send message and wait for response',
|
||||
},
|
||||
],
|
||||
default: 'send',
|
||||
},
|
||||
|
@ -69,4 +76,5 @@ export const description: INodeProperties[] = [
|
|||
...send.description,
|
||||
...deleteMessage.description,
|
||||
...get.description,
|
||||
...sendAndWait.description,
|
||||
];
|
||||
|
|
|
@ -4,60 +4,23 @@ import type {
|
|||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '../../../../../utils/utilities';
|
||||
import {
|
||||
checkAccessToChannel,
|
||||
parseDiscordError,
|
||||
prepareEmbeds,
|
||||
prepareErrorData,
|
||||
prepareMultiPartForm,
|
||||
prepareOptions,
|
||||
sendDiscordMessage,
|
||||
} from '../../helpers/utils';
|
||||
import { discordApiMultiPartRequest, discordApiRequest } from '../../transport';
|
||||
import {
|
||||
embedsFixedCollection,
|
||||
filesFixedCollection,
|
||||
textChannelRLC,
|
||||
userRLC,
|
||||
sendToProperties,
|
||||
} from '../common.description';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Send To',
|
||||
name: 'sendTo',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
},
|
||||
{
|
||||
name: 'Channel',
|
||||
value: 'channel',
|
||||
},
|
||||
],
|
||||
default: 'channel',
|
||||
description: 'Send message to a channel or DM to a user',
|
||||
},
|
||||
|
||||
{
|
||||
...userRLC,
|
||||
displayOptions: {
|
||||
show: {
|
||||
sendTo: ['user'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...textChannelRLC,
|
||||
displayOptions: {
|
||||
show: {
|
||||
sendTo: ['channel'],
|
||||
},
|
||||
},
|
||||
},
|
||||
...sendToProperties,
|
||||
{
|
||||
displayName: 'Message',
|
||||
name: 'content',
|
||||
|
@ -157,90 +120,17 @@ export async function execute(
|
|||
}
|
||||
|
||||
try {
|
||||
const sendTo = this.getNodeParameter('sendTo', i) as string;
|
||||
|
||||
let channelId = '';
|
||||
|
||||
if (sendTo === 'user') {
|
||||
const userId = this.getNodeParameter('userId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
if (isOAuth2) {
|
||||
try {
|
||||
await discordApiRequest.call(this, 'GET', `/guilds/${guildId}/members/${userId}`);
|
||||
} catch (error) {
|
||||
if (error instanceof NodeApiError && error.httpCode === '404') {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`User with the id ${userId} is not a member of the selected guild`,
|
||||
{
|
||||
itemIndex: i,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new NodeOperationError(this.getNode(), error, {
|
||||
itemIndex: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
channelId = (
|
||||
(await discordApiRequest.call(this, 'POST', '/users/@me/channels', {
|
||||
recipient_id: userId,
|
||||
})) as IDataObject
|
||||
).id as string;
|
||||
|
||||
if (!channelId) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Could not create a channel to send direct message to',
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (sendTo === 'channel') {
|
||||
channelId = this.getNodeParameter('channelId', i, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
}
|
||||
|
||||
if (isOAuth2 && sendTo !== 'user') {
|
||||
await checkAccessToChannel.call(this, channelId, userGuilds, i);
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
throw new NodeOperationError(this.getNode(), 'Channel ID is required', { itemIndex: i });
|
||||
}
|
||||
|
||||
let response: IDataObject[] = [];
|
||||
|
||||
if (files?.length) {
|
||||
const multiPartBody = await prepareMultiPartForm.call(this, items, files, body, i);
|
||||
|
||||
response = await discordApiMultiPartRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/channels/${channelId}/messages`,
|
||||
multiPartBody,
|
||||
);
|
||||
} else {
|
||||
response = await discordApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/channels/${channelId}/messages`,
|
||||
returnData.push(
|
||||
...(await sendDiscordMessage.call(this, {
|
||||
guildId,
|
||||
userGuilds,
|
||||
isOAuth2,
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response),
|
||||
{ itemData: { item: i } },
|
||||
items,
|
||||
files,
|
||||
itemIndex: i,
|
||||
})),
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
const err = parseDiscordError.call(this, error, i);
|
||||
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { getSendAndWaitProperties } from '../../../../../utils/sendAndWait/utils';
|
||||
import {
|
||||
createSendAndWaitMessageBody,
|
||||
parseDiscordError,
|
||||
prepareErrorData,
|
||||
sendDiscordMessage,
|
||||
} from '../../helpers/utils';
|
||||
import { sendToProperties } from '../common.description';
|
||||
|
||||
export const description: INodeProperties[] = getSendAndWaitProperties(
|
||||
sendToProperties,
|
||||
'message',
|
||||
undefined,
|
||||
{
|
||||
noButtonStyle: true,
|
||||
defaultApproveLabel: '✓ Approve',
|
||||
defaultDisapproveLabel: '✗ Decline',
|
||||
},
|
||||
).filter((p) => p.name !== 'subject');
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
guildId: string,
|
||||
userGuilds: IDataObject[],
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
const isOAuth2 = this.getNodeParameter('authentication', 0) === 'oAuth2';
|
||||
|
||||
try {
|
||||
await sendDiscordMessage.call(this, {
|
||||
guildId,
|
||||
userGuilds,
|
||||
isOAuth2,
|
||||
body: createSendAndWaitMessageBody(this),
|
||||
items,
|
||||
});
|
||||
} catch (error) {
|
||||
const err = parseDiscordError.call(this, error, 0);
|
||||
|
||||
if (this.continueOnFail()) {
|
||||
return prepareErrorData.call(this, err, 0);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
|
@ -2,7 +2,7 @@ import type { AllEntities } from 'n8n-workflow';
|
|||
|
||||
type NodeMap = {
|
||||
channel: 'get' | 'getAll' | 'create' | 'update' | 'deleteChannel';
|
||||
message: 'deleteMessage' | 'getAll' | 'get' | 'react' | 'send';
|
||||
message: 'deleteMessage' | 'getAll' | 'get' | 'react' | 'send' | 'sendAndWait';
|
||||
member: 'getAll' | 'roleAdd' | 'roleRemove';
|
||||
webhook: 'sendLegacy';
|
||||
};
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import type { IDataObject, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import { NodeOperationError, SEND_AND_WAIT_OPERATION } from 'n8n-workflow';
|
||||
|
||||
import * as channel from './channel';
|
||||
import * as member from './member';
|
||||
import * as message from './message';
|
||||
import type { Discord } from './node.type';
|
||||
import * as webhook from './webhook';
|
||||
import { configureWaitTillDate } from '../../../../utils/sendAndWait/utils';
|
||||
import { checkAccessToGuild } from '../helpers/utils';
|
||||
import { discordApiRequest } from '../transport';
|
||||
|
||||
|
@ -46,6 +47,15 @@ export async function router(this: IExecuteFunctions) {
|
|||
operation,
|
||||
} as Discord;
|
||||
|
||||
if (discord.resource === 'message' && discord.operation === SEND_AND_WAIT_OPERATION) {
|
||||
returnData = await message[discord.operation].execute.call(this, guildId, userGuilds);
|
||||
|
||||
const waitTill = configureWaitTillDate(this);
|
||||
|
||||
await this.putExecutionToWait(waitTill);
|
||||
return [returnData];
|
||||
}
|
||||
|
||||
switch (discord.resource) {
|
||||
case 'channel':
|
||||
returnData = await channel[discord.operation].execute.call(this, guildId, userGuilds);
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as channel from './channel';
|
|||
import * as member from './member';
|
||||
import * as message from './message';
|
||||
import * as webhook from './webhook';
|
||||
import { sendAndWaitWebhooksDescription } from '../../../../utils/sendAndWait/descriptions';
|
||||
|
||||
export const versionDescription: INodeTypeDescription = {
|
||||
displayName: 'Discord',
|
||||
|
@ -19,6 +20,7 @@ export const versionDescription: INodeTypeDescription = {
|
|||
},
|
||||
inputs: [NodeConnectionType.Main],
|
||||
outputs: [NodeConnectionType.Main],
|
||||
webhooks: sendAndWaitWebhooksDescription,
|
||||
credentials: [
|
||||
{
|
||||
name: 'discordBotApi',
|
||||
|
|
|
@ -8,10 +8,11 @@ import type {
|
|||
INode,
|
||||
INodeExecutionData,
|
||||
} from 'n8n-workflow';
|
||||
import { jsonParse, NodeOperationError } from 'n8n-workflow';
|
||||
import { jsonParse, NodeApiError, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { getSendAndWaitConfig } from '../../../../utils/sendAndWait/utils';
|
||||
import { capitalize } from '../../../../utils/utilities';
|
||||
import { discordApiRequest } from '../transport';
|
||||
import { discordApiMultiPartRequest, discordApiRequest } from '../transport';
|
||||
|
||||
export const createSimplifyFunction =
|
||||
(includedFields: string[]) =>
|
||||
|
@ -285,3 +286,141 @@ export async function setupChannelGetter(this: IExecuteFunctions, userGuilds: ID
|
|||
return channelId;
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendDiscordMessage(
|
||||
this: IExecuteFunctions,
|
||||
{
|
||||
guildId,
|
||||
userGuilds,
|
||||
isOAuth2,
|
||||
body,
|
||||
items,
|
||||
files = [],
|
||||
itemIndex = 0,
|
||||
}: {
|
||||
guildId: string;
|
||||
userGuilds: IDataObject[];
|
||||
isOAuth2: boolean;
|
||||
body: IDataObject;
|
||||
items: INodeExecutionData[];
|
||||
files?: IDataObject[];
|
||||
itemIndex?: number;
|
||||
},
|
||||
) {
|
||||
const sendTo = this.getNodeParameter('sendTo', itemIndex) as string;
|
||||
|
||||
let channelId = '';
|
||||
|
||||
if (sendTo === 'user') {
|
||||
const userId = this.getNodeParameter('userId', itemIndex, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
if (isOAuth2) {
|
||||
try {
|
||||
await discordApiRequest.call(this, 'GET', `/guilds/${guildId}/members/${userId}`);
|
||||
} catch (error) {
|
||||
if (error instanceof NodeApiError && error.httpCode === '404') {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`User with the id ${userId} is not a member of the selected guild`,
|
||||
{
|
||||
itemIndex,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw new NodeOperationError(this.getNode(), error, {
|
||||
itemIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
channelId = (
|
||||
(await discordApiRequest.call(this, 'POST', '/users/@me/channels', {
|
||||
recipient_id: userId,
|
||||
})) as IDataObject
|
||||
).id as string;
|
||||
|
||||
if (!channelId) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Could not create a channel to send direct message to',
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (sendTo === 'channel') {
|
||||
channelId = this.getNodeParameter('channelId', itemIndex, undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
}
|
||||
|
||||
if (isOAuth2 && sendTo !== 'user') {
|
||||
await checkAccessToChannel.call(this, channelId, userGuilds, itemIndex);
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
throw new NodeOperationError(this.getNode(), 'Channel ID is required', {
|
||||
itemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
let response: IDataObject[] = [];
|
||||
|
||||
if (files?.length) {
|
||||
const multiPartBody = await prepareMultiPartForm.call(this, items, files, body, itemIndex);
|
||||
|
||||
response = await discordApiMultiPartRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/channels/${channelId}/messages`,
|
||||
multiPartBody,
|
||||
);
|
||||
} else {
|
||||
response = await discordApiRequest.call(this, 'POST', `/channels/${channelId}/messages`, body);
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response),
|
||||
{ itemData: { item: itemIndex } },
|
||||
);
|
||||
|
||||
return executionData;
|
||||
}
|
||||
|
||||
export function createSendAndWaitMessageBody(context: IExecuteFunctions) {
|
||||
const config = getSendAndWaitConfig(context);
|
||||
|
||||
const instanceId = context.getInstanceId();
|
||||
const attributionText = 'This message was sent automatically with ';
|
||||
const link = `https://n8n.io/?utm_source=n8n-internal&utm_medium=powered_by&utm_campaign=${encodeURIComponent(
|
||||
'n8n-nodes-base.telegram',
|
||||
)}${instanceId ? '_' + instanceId : ''}`;
|
||||
const description = `${config.message}\n\n_${attributionText}_[n8n](${link})`;
|
||||
|
||||
const body = {
|
||||
embeds: [
|
||||
{
|
||||
description,
|
||||
color: 5814783,
|
||||
},
|
||||
],
|
||||
components: [
|
||||
{
|
||||
type: 1,
|
||||
components: config.options.map((option) => {
|
||||
return {
|
||||
type: 2,
|
||||
style: 5,
|
||||
label: option.label,
|
||||
url: `${config.url}?approved=${option.value}`,
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return body;
|
||||
}
|
||||
|
|
|
@ -30,5 +30,5 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"alias": ["SMTP"]
|
||||
"alias": ["SMTP", "email", "human", "form", "wait", "hitl", "approval"]
|
||||
}
|
||||
|
|
|
@ -17,5 +17,6 @@
|
|||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlechat/"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"alias": ["human", "form", "wait", "hitl", "approval"]
|
||||
}
|
||||
|
|
|
@ -55,5 +55,5 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"alias": ["email", "human", "form", "wait"]
|
||||
"alias": ["email", "human", "form", "wait", "hitl", "approval"]
|
||||
}
|
||||
|
|
|
@ -18,5 +18,5 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"alias": ["email"]
|
||||
"alias": ["email", "human", "form", "wait", "hitl", "approval"]
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"subcategories": {
|
||||
"HITL": ["Human in the Loop"]
|
||||
},
|
||||
"alias": ["human", "form", "wait"],
|
||||
"alias": ["human", "form", "wait", "hitl", "approval"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"subcategories": {
|
||||
"HITL": ["Human in the Loop"]
|
||||
},
|
||||
"alias": ["human", "form", "wait", "hitl", "approval"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue