From aa53c46367480e31642e807ad1abf149fd13eb28 Mon Sep 17 00:00:00 2001 From: OlegIvaniv Date: Mon, 10 Jul 2023 15:03:21 +0200 Subject: [PATCH] feat(Slack Node): Add option to include link to workflow in Slack node (#6611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(Slack Node): Add “automated by” message to Slack node’s post message Signed-off-by: Oleg Ivaniv * Pass instanceBaseUrl to node context Signed-off-by: Oleg Ivaniv * Move `includeLinkToWorkflow` to options Signed-off-by: Oleg Ivaniv * keep "includeLinkToWorkflow" hidden * Only append the message for version 2.1 and up Signed-off-by: Oleg Ivaniv --------- Signed-off-by: Oleg Ivaniv Co-authored-by: ricardo --- .../cli/src/WorkflowExecuteAdditionalData.ts | 1 + packages/core/src/NodeExecuteFunctions.ts | 1 + .../editor-ui/src/components/NodeSettings.vue | 1 + .../editor-ui/src/plugins/telemetry/index.ts | 20 ++++++- packages/nodes-base/nodes/Slack/Slack.node.ts | 3 +- .../nodes/Slack/V2/GenericFunctions.ts | 60 ++++++++++++++++++- .../nodes/Slack/V2/MessageDescription.ts | 8 +++ .../nodes-base/nodes/Slack/V2/SlackV2.node.ts | 33 +++------- packages/workflow/src/Interfaces.ts | 2 + 9 files changed, 102 insertions(+), 27 deletions(-) diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index 7ea2556438..72c7caa714 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -1179,6 +1179,7 @@ export async function getBase( executeWorkflow, restApiUrl: urlBaseWebhook + config.getEnv('endpoints.rest'), timezone, + instanceBaseUrl: urlBaseWebhook, webhookBaseUrl, webhookWaitingBaseUrl, webhookTestBaseUrl, diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 57a655d235..2c2541285c 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -2161,6 +2161,7 @@ const getCommonWorkflowFunctions = ( getWorkflowStaticData: (type) => workflow.getStaticData(type, node), getRestApiUrl: () => additionalData.restApiUrl, + getInstanceBaseUrl: () => additionalData.instanceBaseUrl, getTimezone: () => getTimezone(workflow, additionalData), }); diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index 504300e164..feb7d0809c 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -800,6 +800,7 @@ export default defineComponent({ this.updateNodeParameterIssues(node, nodeType); this.updateNodeCredentialIssues(node); + this.$telemetry.trackNodeParametersValuesChange(nodeType.name, parameterData); } else { // A property on the node itself changed diff --git a/packages/editor-ui/src/plugins/telemetry/index.ts b/packages/editor-ui/src/plugins/telemetry/index.ts index 4f6b21451a..bd3f630d99 100644 --- a/packages/editor-ui/src/plugins/telemetry/index.ts +++ b/packages/editor-ui/src/plugins/telemetry/index.ts @@ -2,11 +2,12 @@ import type _Vue from 'vue'; import type { ITelemetrySettings, ITelemetryTrackProperties, IDataObject } from 'n8n-workflow'; import type { Route } from 'vue-router'; -import type { INodeCreateElement } from '@/Interface'; +import type { INodeCreateElement, IUpdateInformation } from '@/Interface'; import type { IUserNodesPanelSession } from './telemetry.types'; import { useSettingsStore } from '@/stores/settings.store'; import { useRootStore } from '@/stores/n8nRoot.store'; import { useTelemetryStore } from '@/stores/telemetry.store'; +import { SLACK_NODE_TYPE } from '@/constants'; export class Telemetry { private pageEventQueue: Array<{ route: Route }>; @@ -197,6 +198,23 @@ export class Telemetry { } } + // We currently do not support tracking directly from within node implementation + // so we are using this method as centralized way to track node parameters changes + trackNodeParametersValuesChange(nodeType: string, change: IUpdateInformation) { + if (this.rudderStack) { + switch (nodeType) { + case SLACK_NODE_TYPE: + if (change.name === 'parameters.includeLinkToWorkflow') { + this.track('User toggled n8n reference option'); + } + break; + + default: + break; + } + } + } + private resetNodesPanelSession() { this.userNodesPanelSession.sessionId = `nodes_panel_session_${new Date().valueOf()}`; this.userNodesPanelSession.data = { diff --git a/packages/nodes-base/nodes/Slack/Slack.node.ts b/packages/nodes-base/nodes/Slack/Slack.node.ts index febdacc7f3..16ef542f3b 100644 --- a/packages/nodes-base/nodes/Slack/Slack.node.ts +++ b/packages/nodes-base/nodes/Slack/Slack.node.ts @@ -14,12 +14,13 @@ export class Slack extends VersionedNodeType { group: ['output'], subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', description: 'Consume Slack API', - defaultVersion: 2, + defaultVersion: 2.1, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { 1: new SlackV1(baseDescription), 2: new SlackV2(baseDescription), + 2.1: new SlackV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts b/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts index 1e060563f9..a556713c66 100644 --- a/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Slack/V2/GenericFunctions.ts @@ -7,7 +7,7 @@ import type { IOAuth2Options, } from 'n8n-workflow'; -import { NodeOperationError } from 'n8n-workflow'; +import { NodeOperationError, jsonParse } from 'n8n-workflow'; import get from 'lodash/get'; @@ -130,6 +130,64 @@ export async function slackApiRequestAllItems( return returnData; } +export function getMessageContent( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + i: number, +) { + const nodeVersion = this.getNode().typeVersion; + + const includeLinkToWorkflow = this.getNodeParameter( + 'otherOptions.includeLinkToWorkflow', + i, + nodeVersion >= 2.1 ? true : false, + ) as IDataObject; + + const { id } = this.getWorkflow(); + const automatedMessage = `_Automated with this <${this.getInstanceBaseUrl()}workflow/${id}|n8n workflow>_`; + const messageType = this.getNodeParameter('messageType', i) as string; + + let content: IDataObject = {}; + const text = this.getNodeParameter('text', i, '') as string; + switch (messageType) { + case 'text': + content = { + text: includeLinkToWorkflow ? `${text}\n${automatedMessage}` : text, + }; + break; + case 'block': + content = jsonParse(this.getNodeParameter('blocksUi', i) as string); + + if (includeLinkToWorkflow && Array.isArray(content.blocks)) { + content.blocks.push({ + type: 'section', + text: { + type: 'mrkdwn', + text: automatedMessage, + }, + }); + } + if (text) { + content.text = text; + } + break; + case 'attachment': + content = { attachments: this.getNodeParameter('attachments', i) } as IDataObject; + if (includeLinkToWorkflow && Array.isArray(content.attachments)) { + content.attachments.push({ + text: automatedMessage, + }); + } + break; + default: + throw new NodeOperationError( + this.getNode(), + `The message type "${messageType}" is not known!`, + ); + } + + return content; +} + // tslint:disable-next-line:no-any export function validateJSON(json: string | undefined): any { let result; diff --git a/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts b/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts index c3143e8a11..a16aedece5 100644 --- a/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts @@ -557,6 +557,14 @@ export const messageFields: INodeProperties[] = [ description: 'Other options to set', placeholder: 'Add options', options: [ + { + displayName: 'Include Link To Workflow', + name: 'includeLinkToWorkflow', + type: 'boolean', + default: true, + description: + 'Whether to append a link to this workflow at the end of the message. This is helpful if you have many workflows sending Slack messages.', + }, { displayName: 'Custom Bot Profile Photo', name: 'botProfile', diff --git a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts index 28b243b744..33e022c1e9 100644 --- a/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts +++ b/packages/nodes-base/nodes/Slack/V2/SlackV2.node.ts @@ -24,7 +24,12 @@ import { fileFields, fileOperations } from './FileDescription'; import { reactionFields, reactionOperations } from './ReactionDescription'; import { userGroupFields, userGroupOperations } from './UserGroupDescription'; import { userFields, userOperations } from './UserDescription'; -import { slackApiRequest, slackApiRequestAllItems, validateJSON } from './GenericFunctions'; +import { + slackApiRequest, + slackApiRequestAllItems, + validateJSON, + getMessageContent, +} from './GenericFunctions'; import moment from 'moment'; @@ -34,7 +39,7 @@ export class SlackV2 implements INodeType { constructor(baseDescription: INodeTypeBaseDescription) { this.description = { ...baseDescription, - version: 2, + version: [2, 2.1], defaults: { name: 'Slack', }, @@ -747,7 +752,6 @@ export class SlackV2 implements INodeType { //https://api.slack.com/methods/chat.postMessage if (operation === 'post') { const select = this.getNodeParameter('select', i) as string; - const messageType = this.getNodeParameter('messageType', i) as string; let target = select === 'channel' ? (this.getNodeParameter('channelId', i, undefined, { @@ -764,27 +768,8 @@ export class SlackV2 implements INodeType { target = target.slice(0, 1) === '@' ? target : `@${target}`; } const { sendAsUser } = this.getNodeParameter('otherOptions', i) as IDataObject; - let content: IDataObject = {}; - const text = this.getNodeParameter('text', i, '') as string; - switch (messageType) { - case 'text': - content = { text }; - break; - case 'block': - content = JSON.parse(this.getNodeParameter('blocksUi', i) as string); - if (text) { - content.text = text; - } - break; - case 'attachment': - content = { attachments: this.getNodeParameter('attachments', i) } as IDataObject; - break; - default: - throw new NodeOperationError( - this.getNode(), - `The message type "${messageType}" is not known!`, - ); - } + const content = getMessageContent.call(this, i); + const body: IDataObject = { channel: target, ...content, diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index ecf059438a..3077901e90 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -730,6 +730,7 @@ export interface FunctionsBase { getWorkflowStaticData(type: string): IDataObject; getTimezone(): string; getRestApiUrl(): string; + getInstanceBaseUrl(): string; getMode?: () => WorkflowExecuteMode; getActivationMode?: () => WorkflowActivateMode; @@ -1736,6 +1737,7 @@ export interface IWorkflowExecuteAdditionalData { httpResponse?: express.Response; httpRequest?: express.Request; restApiUrl: string; + instanceBaseUrl: string; setExecutionStatus?: (status: ExecutionStatus) => void; sendMessageToUI?: (source: string, message: any) => void; timezone: string;