From be93028bc8a7b7fe6dc8f3e22ff48b44738b6d65 Mon Sep 17 00:00:00 2001 From: Michael Quinlan Date: Tue, 7 Sep 2021 18:16:31 -0600 Subject: [PATCH 1/7] Adding blocks to slack message update --- .../nodes/Slack/MessageDescription.ts | 23 ++++++++++++++++++- packages/nodes-base/nodes/Slack/Slack.node.ts | 7 ++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index 6174f81dc9c5d..f8a1692adffc5 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -487,7 +487,7 @@ export const messageFields = [ displayName: 'Text', name: 'text', type: 'string', - required: true, + required: false, default: '', displayOptions: { show: { @@ -566,6 +566,27 @@ export const messageFields = [ }, ], }, + { + displayName: 'Blocks', + name: 'blocksJson', + type: 'json', + default: '', + required: false, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ] + }, + }, + description: 'The blocks to add', + }, { displayName: 'Blocks', name: 'blocksUi', diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 8e6e22f013acd..d73c4e037ad04 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -831,7 +831,14 @@ export class Slack implements INodeType { } } body['attachments'] = attachments; + const blocksJson = this.getNodeParameter('blocksJson', i, []) as string; + if (blocksJson !== '' && validateJSON(blocksJson) === undefined) { + throw new NodeOperationError(this.getNode(), 'Blocks it is not a valid json'); + } + if (blocksJson !== '') { + body.blocks = blocksJson; + } // Add all the other options to the request const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; Object.assign(body, updateFields); From 94095113e606724c4c50f7d793a376287ea54db8 Mon Sep 17 00:00:00 2001 From: Michael Quinlan Date: Tue, 7 Sep 2021 18:24:57 -0600 Subject: [PATCH 2/7] Fixing lint --- packages/nodes-base/nodes/Slack/MessageDescription.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index f8a1692adffc5..417feb760d6aa 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -582,7 +582,7 @@ export const messageFields = [ ], operation: [ 'update', - ] + ], }, }, description: 'The blocks to add', From 8dc07c2dd69c960a6f4efb1c51b86216207720fb Mon Sep 17 00:00:00 2001 From: Michael Quinlan Date: Tue, 7 Sep 2021 18:16:31 -0600 Subject: [PATCH 3/7] Adding blocks to slack message update --- .../nodes/Slack/MessageDescription.ts | 23 ++++++++++++++++++- packages/nodes-base/nodes/Slack/Slack.node.ts | 7 ++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index f946a1f795b11..73632132f62e3 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -487,7 +487,7 @@ export const messageFields: INodeProperties[] = [ displayName: 'Text', name: 'text', type: 'string', - required: true, + required: false, default: '', displayOptions: { show: { @@ -566,6 +566,27 @@ export const messageFields: INodeProperties[] = [ }, ], }, + { + displayName: 'Blocks', + name: 'blocksJson', + type: 'json', + default: '', + required: false, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ] + }, + }, + description: 'The blocks to add', + }, { displayName: 'Blocks', name: 'blocksUi', diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index c36ff8908fbc1..f3f2ff2b9cb21 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -839,7 +839,14 @@ export class Slack implements INodeType { } } body['attachments'] = attachments; + const blocksJson = this.getNodeParameter('blocksJson', i, []) as string; + if (blocksJson !== '' && validateJSON(blocksJson) === undefined) { + throw new NodeOperationError(this.getNode(), 'Blocks it is not a valid json'); + } + if (blocksJson !== '') { + body.blocks = blocksJson; + } // Add all the other options to the request const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; Object.assign(body, updateFields); From 4d35f51fa46b0a6775cc6d7f3c431cad1b7260ec Mon Sep 17 00:00:00 2001 From: Michael Quinlan Date: Tue, 7 Sep 2021 18:24:57 -0600 Subject: [PATCH 4/7] Fixing lint --- packages/nodes-base/nodes/Slack/MessageDescription.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index 73632132f62e3..254d8c026cb38 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -582,7 +582,7 @@ export const messageFields: INodeProperties[] = [ ], operation: [ 'update', - ] + ], }, }, description: 'The blocks to add', From bead3b20dc14bd39b6094ef4fd249ef1aa307782 Mon Sep 17 00:00:00 2001 From: Michael Kret Date: Thu, 24 Mar 2022 11:35:11 +0200 Subject: [PATCH 5/7] :zap: added toggle to display json inputs in update operation --- .../nodes/Slack/MessageDescription.ts | 43 +++++++++++++++++++ packages/nodes-base/nodes/Slack/Slack.node.ts | 32 ++++++++++---- 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index e90d74d9d00eb..b2aeef41b7b3e 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -519,6 +519,22 @@ export const messageFields: INodeProperties[] = [ }, description: `Timestamp of the message to be updated.`, }, + { + displayName: 'JSON parameters', + name: 'jsonParameters', + type: 'boolean', + default: false, + displayOptions: { + show: { + operation: [ + 'update', + ], + resource: [ + 'message', + ], + }, + }, + }, { displayName: 'Update Fields', name: 'updateFields', @@ -583,10 +599,37 @@ export const messageFields: INodeProperties[] = [ operation: [ 'update', ], + jsonParameters: [ + true, + ], }, }, description: 'The blocks to add', }, + { + displayName: 'Attachments', + name: 'attachmentsJson', + type: 'json', + default: '', + required: false, + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + resource: [ + 'message', + ], + operation: [ + 'update', + ], + jsonParameters: [ + true, + ], + }, + }, + description: 'The attachments to add', + }, { displayName: 'Blocks', name: 'blocksUi', diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 02b92cfbfd208..260767473543b 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -10,6 +10,7 @@ import { INodePropertyOptions, INodeType, INodeTypeDescription, + JsonObject, NodeOperationError, } from 'n8n-workflow'; @@ -312,7 +313,7 @@ export class Slack implements INodeType { } catch (err) { return { status: 'Error', - message: `${err.message}`, + message: `${(err as JsonObject).message}`, }; } @@ -839,14 +840,29 @@ export class Slack implements INodeType { } } body['attachments'] = attachments; - const blocksJson = this.getNodeParameter('blocksJson', i, []) as string; - if (blocksJson !== '' && validateJSON(blocksJson) === undefined) { - throw new NodeOperationError(this.getNode(), 'Blocks it is not a valid json'); - } - if (blocksJson !== '') { - body.blocks = blocksJson; + const jsonParameters = this.getNodeParameter('jsonParameters', i, false) as boolean; + if (jsonParameters) { + const blocksJson = this.getNodeParameter('blocksJson', i, []) as string; + + if (blocksJson !== '' && validateJSON(blocksJson) === undefined) { + throw new NodeOperationError(this.getNode(), 'Blocks it is not a valid json'); + } + if (blocksJson !== '') { + body.blocks = blocksJson; + } + + const attachmentsJson = this.getNodeParameter('attachmentsJson', i, '') as string; + + if (attachmentsJson !== '' && validateJSON(attachmentsJson) === undefined) { + throw new NodeOperationError(this.getNode(), 'Attachments it is not a valid json'); + } + + if (attachmentsJson !== '') { + body.attachments = attachmentsJson; + } } + // Add all the other options to the request const updateFields = this.getNodeParameter('updateFields', i) as IDataObject; Object.assign(body, updateFields); @@ -1196,7 +1212,7 @@ export class Slack implements INodeType { } } catch (error) { if (this.continueOnFail()) { - returnData.push({ error: error.message }); + returnData.push({ error: (error as JsonObject).message }); continue; } throw error; From 3f1a60592b40e07a86b0b9a53e01b179fb160333 Mon Sep 17 00:00:00 2001 From: ricardo Date: Mon, 18 Apr 2022 17:19:23 -0400 Subject: [PATCH 6/7] :zap: Improvements --- packages/cli/src/CredentialsHelper.ts | 25 ++- .../credentials/SlackApi.credentials.ts | 26 ++- .../nodes/Slack/GenericFunctions.ts | 29 +-- .../nodes/Slack/MessageDescription.ts | 194 +++++++++--------- packages/nodes-base/nodes/Slack/Slack.node.ts | 40 +--- packages/workflow/src/Interfaces.ts | 10 +- 6 files changed, 167 insertions(+), 157 deletions(-) diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 6806dd02943a9..995ce1af8746c 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -6,7 +6,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ import { Credentials, NodeExecuteFunctions } from 'n8n-core'; - +// eslint-disable-next-line import/no-extraneous-dependencies +import { get } from 'lodash'; import { NodeVersionedType } from 'n8n-nodes-base'; import { @@ -626,8 +627,10 @@ export class CredentialsHelper extends ICredentialsHelper { mode, ); + let response: INodeExecutionData[][] | null | undefined; + try { - await routingNode.runNode( + response = await routingNode.runNode( inputData, runIndex, nodeTypeCopy, @@ -676,6 +679,24 @@ export class CredentialsHelper extends ICredentialsHelper { }; } + if ( + credentialTestFunction.testRequest.rules && + Array.isArray(credentialTestFunction.testRequest.rules) + ) { + // Special testing rules are defined so check all in order + for (const rule of credentialTestFunction.testRequest.rules) { + if (rule.type === 'responseSuccessBody') { + const responseData = response![0][0].json; + if (get(responseData, rule.properties.key) === rule.properties.value) { + return { + status: 'Error', + message: rule.properties.message, + }; + } + } + } + } + return { status: 'OK', message: 'Connection successful!', diff --git a/packages/nodes-base/credentials/SlackApi.credentials.ts b/packages/nodes-base/credentials/SlackApi.credentials.ts index ae32dabfb9544..588c4b8cb6883 100644 --- a/packages/nodes-base/credentials/SlackApi.credentials.ts +++ b/packages/nodes-base/credentials/SlackApi.credentials.ts @@ -1,9 +1,11 @@ import { + IAuthenticateBearer, + IAuthenticateQueryAuth, + ICredentialTestRequest, ICredentialType, INodeProperties, } from 'n8n-workflow'; - export class SlackApi implements ICredentialType { name = 'slackApi'; displayName = 'Slack API'; @@ -17,4 +19,26 @@ export class SlackApi implements ICredentialType { required: true, }, ]; + authenticate: IAuthenticateBearer = { + type: 'bearer', + properties: { + tokenPropertyName: 'accessToken', + }, + }; + test: ICredentialTestRequest = { + request: { + baseURL: 'https://slack.com', + url: '/api/users.profile.get', + }, + rules: [ + { + type: 'responseSuccessBody', + properties: { + key: 'ok', + value: false, + message: 'Invalid access token', + }, + }, + ], + }; } diff --git a/packages/nodes-base/nodes/Slack/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/GenericFunctions.ts index e9ee7711772a8..e27bc2741687f 100644 --- a/packages/nodes-base/nodes/Slack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/GenericFunctions.ts @@ -11,6 +11,7 @@ import { import { IDataObject, IOAuth2Options, + JsonObject, NodeApiError, NodeOperationError, } from 'n8n-workflow'; @@ -36,26 +37,16 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu if (Object.keys(query).length === 0) { delete options.qs; } - try { - let response: any; // tslint:disable-line:no-any - if (authenticationMethod === 'accessToken') { - const credentials = await this.getCredentials('slackApi'); - if (credentials === undefined) { - throw new NodeOperationError(this.getNode(), 'No credentials got returned!'); - } - options.headers!.Authorization = `Bearer ${credentials.accessToken}`; - //@ts-ignore - response = await this.helpers.request(options); - } else { + const oAuth2Options: IOAuth2Options = { + tokenType: 'Bearer', + property: 'authed_user.access_token', + }; - const oAuth2Options: IOAuth2Options = { - tokenType: 'Bearer', - property: 'authed_user.access_token', - }; - //@ts-ignore - response = await this.helpers.requestOAuth2.call(this, 'slackOAuth2Api', options, oAuth2Options); - } + try { + let response: any; // tslint:disable-line:no-any + const credentialType = authenticationMethod === 'accessToken' ? 'slackApi' : 'slackOAuth2Api'; + response = await this.helpers.requestWithAuthentication.call(this, credentialType, options, { oauth2: oAuth2Options }); if (response.ok === false) { if (response.error === 'paid_teams_only') { @@ -69,7 +60,7 @@ export async function slackApiRequest(this: IExecuteFunctions | IExecuteSingleFu return response; } catch (error) { - throw new NodeApiError(this.getNode(), error); + throw new NodeApiError(this.getNode(), error as JsonObject); } } diff --git a/packages/nodes-base/nodes/Slack/MessageDescription.ts b/packages/nodes-base/nodes/Slack/MessageDescription.ts index b2aeef41b7b3e..27e1c4aa8fbde 100644 --- a/packages/nodes-base/nodes/Slack/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/MessageDescription.ts @@ -171,6 +171,97 @@ export const messageFields: INodeProperties[] = [ }, }, }, + { + displayName: 'Options', + name: 'otherOptions', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'post', + 'postEphemeral', + ], + resource: [ + 'message', + ], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add options', + options: [ + { + displayName: 'Icon Emoji', + name: 'icon_emoji', + type: 'string', + default: '', + description: 'Emoji to use as the icon for this message. Overrides icon_url.', + }, + { + displayName: 'Icon URL', + name: 'icon_url', + type: 'string', + default: '', + description: 'URL to an image to use as the icon for this message.', + }, + { + displayName: 'Link Names', + name: 'link_names', + type: 'boolean', + default: false, + description: 'Find and link channel names and usernames.', + }, + { + displayName: 'Make Reply', + name: 'thread_ts', + type: 'string', + default: '', + description: 'Provide another message\'s ts value to make this message a reply.', + }, + { + displayName: 'Markdown', + name: 'mrkdwn', + type: 'boolean', + default: true, + description: 'Use Slack Markdown parsing.', + }, + { + displayName: 'Reply Broadcast', + name: 'reply_broadcast', + type: 'boolean', + default: false, + description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.', + }, + { + displayName: 'Unfurl Links', + name: 'unfurl_links', + type: 'boolean', + default: false, + description: 'Pass true to enable unfurling of primarily text-based content.', + }, + { + displayName: 'Unfurl Media', + name: 'unfurl_media', + type: 'boolean', + default: true, + description: 'Pass false to disable unfurling of media content.', + }, + { + displayName: 'Send as User', + name: 'sendAsUser', + type: 'string', + displayOptions: { + show: { + '/authentication': [ + 'accessToken', + ], + }, + }, + default: '', + description: 'The message will be sent from this username (i.e. as if this individual sent the message).', + }, + ], + }, { displayName: 'Attachments', name: 'attachments', @@ -367,97 +458,6 @@ export const messageFields: INodeProperties[] = [ }, ], }, - { - displayName: 'Other Options', - name: 'otherOptions', - type: 'collection', - displayOptions: { - show: { - operation: [ - 'post', - 'postEphemeral', - ], - resource: [ - 'message', - ], - }, - }, - default: {}, - description: 'Other options to set', - placeholder: 'Add options', - options: [ - { - displayName: 'Icon Emoji', - name: 'icon_emoji', - type: 'string', - default: '', - description: 'Emoji to use as the icon for this message. Overrides icon_url.', - }, - { - displayName: 'Icon URL', - name: 'icon_url', - type: 'string', - default: '', - description: 'URL to an image to use as the icon for this message.', - }, - { - displayName: 'Link Names', - name: 'link_names', - type: 'boolean', - default: false, - description: 'Find and link channel names and usernames.', - }, - { - displayName: 'Make Reply', - name: 'thread_ts', - type: 'string', - default: '', - description: 'Provide another message\'s ts value to make this message a reply.', - }, - { - displayName: 'Markdown', - name: 'mrkdwn', - type: 'boolean', - default: true, - description: 'Use Slack Markdown parsing.', - }, - { - displayName: 'Reply Broadcast', - name: 'reply_broadcast', - type: 'boolean', - default: false, - description: 'Used in conjunction with thread_ts and indicates whether reply should be made visible to everyone in the channel or conversation.', - }, - { - displayName: 'Unfurl Links', - name: 'unfurl_links', - type: 'boolean', - default: false, - description: 'Pass true to enable unfurling of primarily text-based content.', - }, - { - displayName: 'Unfurl Media', - name: 'unfurl_media', - type: 'boolean', - default: true, - description: 'Pass false to disable unfurling of media content.', - }, - { - displayName: 'Send as User', - name: 'sendAsUser', - type: 'string', - displayOptions: { - show: { - '/authentication': [ - 'accessToken', - ], - }, - }, - default: '', - description: 'The message will be sent from this username (i.e. as if this individual sent the message).', - }, - ], - }, /* ----------------------------------------------------------------------- */ /* message:update */ @@ -583,8 +583,8 @@ export const messageFields: INodeProperties[] = [ ], }, { - displayName: 'Blocks', - name: 'blocksJson', + displayName: 'Attachments', + name: 'attachmentsJson', type: 'json', default: '', required: false, @@ -604,11 +604,11 @@ export const messageFields: INodeProperties[] = [ ], }, }, - description: 'The blocks to add', + description: 'The attachments to add', }, { - displayName: 'Attachments', - name: 'attachmentsJson', + displayName: 'Blocks', + name: 'blocksJson', type: 'json', default: '', required: false, @@ -628,7 +628,7 @@ export const messageFields: INodeProperties[] = [ ], }, }, - description: 'The attachments to add', + description: 'The blocks to add', }, { displayName: 'Blocks', diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index 260767473543b..acac54fb233b1 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -1,4 +1,6 @@ -import { IExecuteFunctions } from 'n8n-core'; +import { + IExecuteFunctions, +} from 'n8n-core'; import { ICredentialsDecrypted, @@ -131,7 +133,6 @@ export class Slack implements INodeType { ], }, }, - testedBy: 'testSlackTokenAuth', }, { name: 'slackOAuth2Api', @@ -288,41 +289,6 @@ export class Slack implements INodeType { return returnData; }, }, - credentialTest: { - async testSlackTokenAuth(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise { - - const options = { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - Authorization: `Bearer ${credential.data!.accessToken}`, - }, - uri: 'https://slack.com/api/users.profile.get', - json: true, - }; - - try { - const response = await this.helpers.request(options); - - if (!response.ok) { - return { - status: 'Error', - message: `${response.error}`, - }; - } - } catch (err) { - return { - status: 'Error', - message: `${(err as JsonObject).message}`, - }; - } - - return { - status: 'OK', - message: 'Connection successful!', - }; - }, - }, }; async execute(this: IExecuteFunctions): Promise { diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 058b471c43375..4430c302da4ed 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -246,9 +246,17 @@ export interface IAuthenticateRuleResponseCode extends IAuthenticateRuleBase { }; } +export interface IAuthenticateRuleResponseSuccessBody extends IAuthenticateRuleBase { + type: 'responseSuccessBody'; + properties: { + message: string; + key: string; + value: any; + }; +} export interface ICredentialTestRequest { request: IHttpRequestOptions; - rules?: IAuthenticateRuleResponseCode[]; + rules?: IAuthenticateRuleResponseCode[] | IAuthenticateRuleResponseSuccessBody[]; } export interface ICredentialTestRequestData { From 5d1ddb0e9b56d999ec4d9278b81262aafceb43a9 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Tue, 19 Apr 2022 03:24:10 -0400 Subject: [PATCH 7/7] feat(Markdown Node): Add new node to covert between Markdown <> HTML (#1728) * :sparkles: Markdown Node * Tweaked wording * :arrow_up: Bump showdown to latest version * :zap: Small improvement * :shirt: Fix linting issue * :zap: Small improvements * :hammer: added options, added continue on fail, some clean up * :zap: removed test code * :zap: added missing semicolumn * :hammer: wip * :hammer: replaced library for converting html to markdown, added options * :zap: lock file fix * :hammer: clean up Co-authored-by: sirdavidoff <1670123+sirdavidoff@users.noreply.github.com> Co-authored-by: Michael Kret --- .../nodes/Markdown/Markdown.node.ts | 608 ++++++++++++++++++ .../nodes-base/nodes/Markdown/markdown.svg | 6 + packages/nodes-base/package.json | 4 + 3 files changed, 618 insertions(+) create mode 100644 packages/nodes-base/nodes/Markdown/Markdown.node.ts create mode 100644 packages/nodes-base/nodes/Markdown/markdown.svg diff --git a/packages/nodes-base/nodes/Markdown/Markdown.node.ts b/packages/nodes-base/nodes/Markdown/Markdown.node.ts new file mode 100644 index 0000000000000..8a5bec5415e4e --- /dev/null +++ b/packages/nodes-base/nodes/Markdown/Markdown.node.ts @@ -0,0 +1,608 @@ +import { IExecuteFunctions } from 'n8n-core'; + +import { + IDataObject, + INodeExecutionData, + INodeType, + INodeTypeDescription, + JsonObject, +} from 'n8n-workflow'; + +import { Converter } from 'showdown'; + +import { NodeHtmlMarkdown } from 'node-html-markdown'; + +import { isEmpty, set } from 'lodash'; + +export class Markdown implements INodeType { + description: INodeTypeDescription = { + displayName: 'Markdown', + name: 'markdown', + icon: 'file:markdown.svg', + group: ['output'], + version: 1, + subtitle: '={{$parameter["mode"]==="markdownToHtml" ? "Markdown to HTML" : "HTML to Markdown"}}', + description: 'Convert data between Markdown and HTML', + defaults: { + name: 'Markdown', + color: '#000000', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [], + properties: [ + { + displayName: 'Mode', + name: 'mode', + type: 'options', + options: [ + { + name: 'Markdown to HTML', + value: 'markdownToHtml', + description: 'Convert data from Markdown to HTML', + }, + { + name: 'HTML to Markdown', + value: 'htmlToMarkdown', + description: 'Convert data from HTML to Markdown', + }, + ], + default: 'htmlToMarkdown', + }, + { + displayName: 'HTML', + name: 'html', + type: 'string', + displayOptions: { + show: { + mode: ['htmlToMarkdown'], + }, + }, + default: '', + required: true, + description: 'The HTML to be converted to markdown', + }, + { + displayName: 'Markdown', + name: 'markdown', + type: 'string', + typeOptions: { + alwaysOpenEditWindow: true, + }, + displayOptions: { + show: { + mode: ['markdownToHtml'], + }, + }, + default: '', + required: true, + description: 'The Markdown to be converted to html', + }, + { + displayName: 'Destination Key', + name: 'destinationKey', + type: 'string', + displayOptions: { + show: { + mode: ['markdownToHtml', 'htmlToMarkdown'], + }, + }, + default: 'data', + required: true, + placeholder: '', + description: + 'The field to put the output in. Specify nested fields using dots, e.g."level1.level2.newKey".', + }, + + //============= HTML to Markdown Options =============== + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + mode: ['htmlToMarkdown'], + }, + }, + options: [ + { + displayName: 'Bullet Marker', + name: 'bulletMarker', + type: 'string', + default: '*', + description: 'Specify bullet marker, default *', + }, + { + displayName: 'Code Block Fence', + name: 'codeFence', + type: 'string', + default: '```', + description: 'Specify code block fence, default ```', + }, + { + displayName: 'Emphasis Delimiter', + name: 'emDelimiter', + type: 'string', + default: '_', + description: 'Specify emphasis delimiter, default _', + }, + { + displayName: 'Global Escape Pattern', + name: 'globalEscape', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + description: + 'Setting this will override the default escape settings, you might want to use textReplace option instead', + options: [ + { + name: 'value', + displayName: 'Value', + values: [ + { + displayName: 'Pattern', + name: 'pattern', + type: 'string', + default: '', + description: 'RegEx for pattern', + }, + { + displayName: 'Replacement', + name: 'replacement', + type: 'string', + default: '', + description: 'String replacement', + }, + ], + }, + ], + }, + { + displayName: 'Ignored Elements', + name: 'ignore', + type: 'string', + default: '', + description: + 'Supplied elements will be ignored (ignores inner text does not parse children)', + placeholder: 'e.g. h1, p ...', + hint: 'Comma separated elements', + }, + { + displayName: 'Keep Images With Data', + name: 'keepDataImages', + type: 'boolean', + default: false, + description: + 'Whether to keep images with data: URI (Note: These can be up to 1MB each), e.g. .', + }, + { + displayName: 'Line Start Escape Pattern', + name: 'lineStartEscape', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + description: + 'Setting this will override the default escape settings, you might want to use textReplace option instead', + options: [ + { + name: 'value', + displayName: 'Value', + values: [ + { + displayName: 'Pattern', + name: 'pattern', + type: 'string', + default: '', + description: 'RegEx for pattern', + }, + { + displayName: 'Replacement', + name: 'replacement', + type: 'string', + default: '', + description: 'String replacement', + }, + ], + }, + ], + }, + { + displayName: 'Max Consecutive New Lines', + name: 'maxConsecutiveNewlines', + type: 'number', + default: 3, + description: 'Specify max consecutive new lines allowed', + }, + { + displayName: 'Place URLs At The Bottom', + name: 'useLinkReferenceDefinitions', + type: 'boolean', + default: false, + description: + 'Whether to Place URLS at the bottom and format links using link reference definitions', + }, + { + displayName: 'Strong Delimiter', + name: 'strongDelimiter', + type: 'string', + default: '**', + description: 'Specify strong delimiter, default **', + }, + { + displayName: 'Style For Code Block', + name: 'codeBlockStyle', + type: 'options', + default: 'fence', + description: 'Specify style for code block, default "fence"', + options: [ + { + name: 'Fence', + value: 'fence', + }, + { + name: 'Indented', + value: 'indented', + }, + ], + }, + { + displayName: 'Text Replacement Pattern', + name: 'textReplace', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: [], + description: + 'User-defined text replacement pattern (Replaces matching text retrieved from nodes)', + options: [ + { + name: 'values', + displayName: 'Values', + values: [ + { + displayName: 'Pattern', + name: 'pattern', + type: 'string', + default: '', + description: 'RegEx for pattern', + }, + { + displayName: 'Replacement', + name: 'replacement', + type: 'string', + default: '', + description: 'String replacement', + }, + ], + }, + ], + }, + { + displayName: 'Treat As Blocks', + name: 'blockElements', + type: 'string', + default: '', + description: + 'Supplied elements will be treated as blocks (surrounded with blank lines)', + placeholder: 'e.g. p, div, ...', + hint: 'Comma separated elements', + }, + ], + }, + //============= Markdown to HTML Options =============== + { + displayName: 'Options', + name: 'options', + type: 'collection', + placeholder: 'Add Option', + default: {}, + displayOptions: { + show: { + mode: ['markdownToHtml'], + }, + }, + options: [ + { + displayName: 'Add Blank To Links', + name: 'openLinksInNewWindow', + type: 'boolean', + default: false, + description: + 'Whether to open all links in new windows (by adding the attribute target="_blank" to tags)', + }, + { + displayName: 'Automatic Linking To URLs', + name: 'simplifiedAutoLink', + type: 'boolean', + default: false, + description: 'Whether to enable automatic linking to urls', + }, + { + displayName: 'Backslash Escapes HTML Tags', + name: 'backslashEscapesHTMLTags', + type: 'boolean', + default: false, + description: 'Whether to support for HTML Tag escaping ex:
foo, and tags instead of an HTML fragment', + }, + { + displayName: 'Customized Header ID', + name: 'customizedHeaderId', + type: 'boolean', + default: false, + description: 'Whether to use text in curly braces as header id', + }, + { + displayName: 'Emoji Support', + name: 'emoji', + type: 'boolean', + default: false, + description: + 'Whether to enable emoji support. Ex: this is a :smile: emoji For more info on available emojis, see https://github.com/showdownjs/showdown/wiki/Emojis.', + }, + { + displayName: 'Encode Emails', + name: 'encodeEmails', + type: 'boolean', + default: true, + description: + 'Whether to enable e-mail addresses encoding through the use of Character Entities, transforming ASCII e-mail addresses into its equivalent decimal entities', + }, + { + displayName: 'Exclude Trailing Punctuation From URLs', + name: 'excludeTrailingPunctuationFromURLs', + type: 'boolean', + default: false, + description: + 'Whether to exclude trailing punctuation from autolinking urls. Punctuation excluded: . ! ? ( ). Only applies if simplifiedAutoLink option is set to true.', + }, + { + displayName: 'GitHub Code Blocks', + name: 'ghCodeBlocks', + type: 'boolean', + default: true, + description: 'Whether to enable support for GFM code block style', + }, + { + displayName: 'GitHub Compatible Header IDs', + name: 'ghCompatibleHeaderId', + type: 'boolean', + default: false, + description: + 'Whether to generate header ids compatible with github style (spaces are replaced with dashes and a bunch of non alphanumeric chars are removed)', + }, + { + displayName: 'GitHub Mention Link', + name: 'ghMentionsLink', + type: 'string', + default: 'https://github.com/{u}', + description: + 'Whether to change the link generated by @mentions. Showdown will replace {u} with the username. Only applies if ghMentions option is enabled.', + }, + { + displayName: 'GitHub Mentions', + name: 'ghMentions', + type: 'boolean', + default: false, + description: 'Whether to enable github @mentions, which link to the username mentioned', + }, + { + displayName: 'GitHub Task Lists', + name: 'tasklists', + type: 'boolean', + default: false, + description: 'Whether to enable support for GFM tasklists', + }, + { + displayName: 'Header Level Start', + name: 'headerLevelStart', + type: 'number', + default: 1, + description: 'Whether to set the header starting level', + }, + { + displayName: 'Mandatory Space Before Header', + name: 'requireSpaceBeforeHeadingText', + type: 'boolean', + default: false, + description: 'Whether to make adding a space between # and the header text mandatory', + }, + { + displayName: 'Middle Word Asterisks', + name: 'literalMidWordAsterisks', + type: 'boolean', + default: false, + description: + 'Whether to stop showdown from interpreting asterisks in the middle of words as and and instead treat them as literal asterisks', + }, + { + displayName: 'Middle Word Underscores', + name: 'literalMidWordUnderscores', + type: 'boolean', + default: false, + description: + 'Whether to stop showdown from interpreting underscores in the middle of words as and and instead treat them as literal underscores', + }, + { + displayName: 'No Header ID', + name: 'noHeaderId', + type: 'boolean', + default: false, + description: 'Whether to disable the automatic generation of header ids', + }, + { + displayName: 'Parse Image Dimensions', + name: 'parseImgDimensions', + type: 'boolean', + default: false, + description: + 'Whether to enable support for setting image dimensions from within markdown syntax', + }, + { + displayName: 'Prefix Header ID', + name: 'prefixHeaderId', + type: 'string', + default: 'section', + description: 'Add a prefix to the generated header ids', + }, + { + displayName: 'Raw Header ID', + name: 'rawHeaderId', + type: 'boolean', + default: false, + description: + 'Whether to remove only spaces, \' and " from generated header ids (including prefixes), replacing them with dashes (-)', + }, + { + displayName: 'Raw Prefix Header ID', + name: 'rawPrefixHeaderId', + type: 'boolean', + default: false, + description: 'Whether to prevent showdown from modifying the prefix', + }, + { + displayName: 'Simple Line Breaks', + name: 'simpleLineBreaks', + type: 'boolean', + default: false, + description: + 'Whether to parse line breaks as
, like GitHub does, without needing 2 spaces at the end of the line', + }, + { + displayName: 'Smart Indentation Fix', + name: 'smartIndentationFix', + type: 'boolean', + default: false, + description: + 'Whether to try to smartly fix indentation problems related to es6 template strings in the midst of indented code', + }, + { + displayName: 'Spaces Indented Sublists', + name: 'disableForced4SpacesIndentedSublists', + type: 'boolean', + default: false, + description: + 'Whether to disable the requirement of indenting sublists by 4 spaces for them to be nested, effectively reverting to the old behavior where 2 or 3 spaces were enough', + }, + { + displayName: 'Split Adjacent Blockquotes', + name: 'splitAdjacentBlockquotes', + type: 'boolean', + default: false, + description: 'Whether to split adjacent blockquote blocks', + }, + { + displayName: 'Strikethrough', + name: 'strikethrough', + type: 'boolean', + default: false, + description: 'Whether to enable support for strikethrough syntax', + }, + { + displayName: 'Tables Header ID', + name: 'tablesHeaderId', + type: 'boolean', + default: false, + description: 'Whether to add an ID property to table headers tags', + }, + { + displayName: 'Tables Support', + name: 'tables', + type: 'boolean', + default: false, + description: 'Whether to enable support for tables syntax', + }, + ], + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: IDataObject[] = []; + const mode = this.getNodeParameter('mode', 0) as string; + + const { length } = items; + for (let i = 0; i < length; i++) { + try { + if (mode === 'htmlToMarkdown') { + const options = this.getNodeParameter('options', i) as IDataObject; + const destinationKey = this.getNodeParameter('destinationKey', i) as string; + + const textReplaceOption = this.getNodeParameter('options.textReplace.values', i, []) as IDataObject[]; + options.textReplace = !isEmpty(textReplaceOption) + ? textReplaceOption.map((entry) => [entry.pattern, entry.replacement]) + : undefined; + + const lineStartEscapeOption = this.getNodeParameter('options.lineStartEscape.value', i, {}) as IDataObject; + options.lineStartEscape = !isEmpty(lineStartEscapeOption) + ? [lineStartEscapeOption.pattern, lineStartEscapeOption.replacement] + : undefined; + + const globalEscapeOption = this.getNodeParameter('options.globalEscape.value', i, {}) as IDataObject; + options.globalEscape = !isEmpty(globalEscapeOption) + ? [globalEscapeOption.pattern, globalEscapeOption.replacement] + : undefined; + + options.ignore = options.ignore + ? (options.ignore as string).split(',').map(element => element.trim()) : undefined; + options.blockElements = options.blockElements + ? (options.blockElements as string).split(',').map(element => element.trim()) : undefined; + + const markdownOptions = {} as IDataObject; + + Object.keys(options).forEach((option) => { + if (options[option]) { + markdownOptions[option] = options[option]; + } + }); + + const html = this.getNodeParameter('html', i) as string; + + const markdownFromHTML = NodeHtmlMarkdown.translate(html, markdownOptions); + + const newItem = JSON.parse(JSON.stringify(items[i].json)); + set(newItem, destinationKey, markdownFromHTML); + returnData.push(newItem); + } + + if (mode === 'markdownToHtml') { + const markdown = this.getNodeParameter('markdown', i) as string; + const destinationKey = this.getNodeParameter('destinationKey', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + + const converter = new Converter(); + + Object.keys(options).forEach((key) => converter.setOption(key, options[key])); + const htmlFromMarkdown = converter.makeHtml(markdown); + + const newItem = JSON.parse(JSON.stringify(items[i].json)); + set(newItem, destinationKey, htmlFromMarkdown); + + returnData.push(newItem); + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ error: (error as JsonObject).message }); + continue; + } + throw error; + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Markdown/markdown.svg b/packages/nodes-base/nodes/Markdown/markdown.svg new file mode 100644 index 0000000000000..24f62fb3298bf --- /dev/null +++ b/packages/nodes-base/nodes/Markdown/markdown.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index a3d4d5bf697fc..133ecd88236b2 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -515,6 +515,7 @@ "dist/nodes/Mailjet/Mailjet.node.js", "dist/nodes/Mailjet/MailjetTrigger.node.js", "dist/nodes/Mandrill/Mandrill.node.js", + "dist/nodes/Markdown/Markdown.node.js", "dist/nodes/Marketstack/Marketstack.node.js", "dist/nodes/Matrix/Matrix.node.js", "dist/nodes/Mattermost/Mattermost.node.js", @@ -707,6 +708,7 @@ "@types/nodemailer": "^6.4.0", "@types/redis": "^2.8.11", "@types/request-promise-native": "~1.0.15", + "@types/showdown": "^1.9.4", "@types/ssh2-sftp-client": "^5.1.0", "@types/tmp": "^0.2.0", "@types/uuid": "^8.3.2", @@ -756,6 +758,7 @@ "mssql": "^6.2.0", "mysql2": "~2.3.0", "n8n-core": "~0.113.0", + "node-html-markdown": "^1.1.3", "node-ssh": "^12.0.0", "nodemailer": "^6.5.0", "pdf-parse": "^1.1.1", @@ -766,6 +769,7 @@ "request": "^2.88.2", "rhea": "^1.0.11", "rss-parser": "^3.7.0", + "showdown": "^2.0.3", "simple-git": "^3.5.0", "snowflake-sdk": "^1.5.3", "ssh2-sftp-client": "^7.0.0",