mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(Slack Node): Add option to include link to workflow in Slack node (#6611)
* feat(Slack Node): Add “automated by” message to Slack node’s post message Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * Pass instanceBaseUrl to node context Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * Move `includeLinkToWorkflow` to options Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> * keep "includeLinkToWorkflow" hidden * Only append the message for version 2.1 and up Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
This commit is contained in:
parent
d617f63ae9
commit
aa53c46367
|
@ -1179,6 +1179,7 @@ export async function getBase(
|
||||||
executeWorkflow,
|
executeWorkflow,
|
||||||
restApiUrl: urlBaseWebhook + config.getEnv('endpoints.rest'),
|
restApiUrl: urlBaseWebhook + config.getEnv('endpoints.rest'),
|
||||||
timezone,
|
timezone,
|
||||||
|
instanceBaseUrl: urlBaseWebhook,
|
||||||
webhookBaseUrl,
|
webhookBaseUrl,
|
||||||
webhookWaitingBaseUrl,
|
webhookWaitingBaseUrl,
|
||||||
webhookTestBaseUrl,
|
webhookTestBaseUrl,
|
||||||
|
|
|
@ -2161,6 +2161,7 @@ const getCommonWorkflowFunctions = (
|
||||||
getWorkflowStaticData: (type) => workflow.getStaticData(type, node),
|
getWorkflowStaticData: (type) => workflow.getStaticData(type, node),
|
||||||
|
|
||||||
getRestApiUrl: () => additionalData.restApiUrl,
|
getRestApiUrl: () => additionalData.restApiUrl,
|
||||||
|
getInstanceBaseUrl: () => additionalData.instanceBaseUrl,
|
||||||
getTimezone: () => getTimezone(workflow, additionalData),
|
getTimezone: () => getTimezone(workflow, additionalData),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -800,6 +800,7 @@ export default defineComponent({
|
||||||
|
|
||||||
this.updateNodeParameterIssues(node, nodeType);
|
this.updateNodeParameterIssues(node, nodeType);
|
||||||
this.updateNodeCredentialIssues(node);
|
this.updateNodeCredentialIssues(node);
|
||||||
|
this.$telemetry.trackNodeParametersValuesChange(nodeType.name, parameterData);
|
||||||
} else {
|
} else {
|
||||||
// A property on the node itself changed
|
// A property on the node itself changed
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,12 @@ import type _Vue from 'vue';
|
||||||
import type { ITelemetrySettings, ITelemetryTrackProperties, IDataObject } from 'n8n-workflow';
|
import type { ITelemetrySettings, ITelemetryTrackProperties, IDataObject } from 'n8n-workflow';
|
||||||
import type { Route } from 'vue-router';
|
import type { Route } from 'vue-router';
|
||||||
|
|
||||||
import type { INodeCreateElement } from '@/Interface';
|
import type { INodeCreateElement, IUpdateInformation } from '@/Interface';
|
||||||
import type { IUserNodesPanelSession } from './telemetry.types';
|
import type { IUserNodesPanelSession } from './telemetry.types';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||||
import { useTelemetryStore } from '@/stores/telemetry.store';
|
import { useTelemetryStore } from '@/stores/telemetry.store';
|
||||||
|
import { SLACK_NODE_TYPE } from '@/constants';
|
||||||
|
|
||||||
export class Telemetry {
|
export class Telemetry {
|
||||||
private pageEventQueue: Array<{ route: Route }>;
|
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() {
|
private resetNodesPanelSession() {
|
||||||
this.userNodesPanelSession.sessionId = `nodes_panel_session_${new Date().valueOf()}`;
|
this.userNodesPanelSession.sessionId = `nodes_panel_session_${new Date().valueOf()}`;
|
||||||
this.userNodesPanelSession.data = {
|
this.userNodesPanelSession.data = {
|
||||||
|
|
|
@ -14,12 +14,13 @@ export class Slack extends VersionedNodeType {
|
||||||
group: ['output'],
|
group: ['output'],
|
||||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||||
description: 'Consume Slack API',
|
description: 'Consume Slack API',
|
||||||
defaultVersion: 2,
|
defaultVersion: 2.1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||||
1: new SlackV1(baseDescription),
|
1: new SlackV1(baseDescription),
|
||||||
2: new SlackV2(baseDescription),
|
2: new SlackV2(baseDescription),
|
||||||
|
2.1: new SlackV2(baseDescription),
|
||||||
};
|
};
|
||||||
|
|
||||||
super(nodeVersions, baseDescription);
|
super(nodeVersions, baseDescription);
|
||||||
|
|
|
@ -7,7 +7,7 @@ import type {
|
||||||
IOAuth2Options,
|
IOAuth2Options,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { NodeOperationError } from 'n8n-workflow';
|
import { NodeOperationError, jsonParse } from 'n8n-workflow';
|
||||||
|
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|
||||||
|
@ -130,6 +130,64 @@ export async function slackApiRequestAllItems(
|
||||||
return returnData;
|
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
|
// tslint:disable-next-line:no-any
|
||||||
export function validateJSON(json: string | undefined): any {
|
export function validateJSON(json: string | undefined): any {
|
||||||
let result;
|
let result;
|
||||||
|
|
|
@ -557,6 +557,14 @@ export const messageFields: INodeProperties[] = [
|
||||||
description: 'Other options to set',
|
description: 'Other options to set',
|
||||||
placeholder: 'Add options',
|
placeholder: 'Add options',
|
||||||
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',
|
displayName: 'Custom Bot Profile Photo',
|
||||||
name: 'botProfile',
|
name: 'botProfile',
|
||||||
|
|
|
@ -24,7 +24,12 @@ import { fileFields, fileOperations } from './FileDescription';
|
||||||
import { reactionFields, reactionOperations } from './ReactionDescription';
|
import { reactionFields, reactionOperations } from './ReactionDescription';
|
||||||
import { userGroupFields, userGroupOperations } from './UserGroupDescription';
|
import { userGroupFields, userGroupOperations } from './UserGroupDescription';
|
||||||
import { userFields, userOperations } from './UserDescription';
|
import { userFields, userOperations } from './UserDescription';
|
||||||
import { slackApiRequest, slackApiRequestAllItems, validateJSON } from './GenericFunctions';
|
import {
|
||||||
|
slackApiRequest,
|
||||||
|
slackApiRequestAllItems,
|
||||||
|
validateJSON,
|
||||||
|
getMessageContent,
|
||||||
|
} from './GenericFunctions';
|
||||||
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
|
@ -34,7 +39,7 @@ export class SlackV2 implements INodeType {
|
||||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||||
this.description = {
|
this.description = {
|
||||||
...baseDescription,
|
...baseDescription,
|
||||||
version: 2,
|
version: [2, 2.1],
|
||||||
defaults: {
|
defaults: {
|
||||||
name: 'Slack',
|
name: 'Slack',
|
||||||
},
|
},
|
||||||
|
@ -747,7 +752,6 @@ export class SlackV2 implements INodeType {
|
||||||
//https://api.slack.com/methods/chat.postMessage
|
//https://api.slack.com/methods/chat.postMessage
|
||||||
if (operation === 'post') {
|
if (operation === 'post') {
|
||||||
const select = this.getNodeParameter('select', i) as string;
|
const select = this.getNodeParameter('select', i) as string;
|
||||||
const messageType = this.getNodeParameter('messageType', i) as string;
|
|
||||||
let target =
|
let target =
|
||||||
select === 'channel'
|
select === 'channel'
|
||||||
? (this.getNodeParameter('channelId', i, undefined, {
|
? (this.getNodeParameter('channelId', i, undefined, {
|
||||||
|
@ -764,27 +768,8 @@ export class SlackV2 implements INodeType {
|
||||||
target = target.slice(0, 1) === '@' ? target : `@${target}`;
|
target = target.slice(0, 1) === '@' ? target : `@${target}`;
|
||||||
}
|
}
|
||||||
const { sendAsUser } = this.getNodeParameter('otherOptions', i) as IDataObject;
|
const { sendAsUser } = this.getNodeParameter('otherOptions', i) as IDataObject;
|
||||||
let content: IDataObject = {};
|
const content = getMessageContent.call(this, i);
|
||||||
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 body: IDataObject = {
|
const body: IDataObject = {
|
||||||
channel: target,
|
channel: target,
|
||||||
...content,
|
...content,
|
||||||
|
|
|
@ -730,6 +730,7 @@ export interface FunctionsBase {
|
||||||
getWorkflowStaticData(type: string): IDataObject;
|
getWorkflowStaticData(type: string): IDataObject;
|
||||||
getTimezone(): string;
|
getTimezone(): string;
|
||||||
getRestApiUrl(): string;
|
getRestApiUrl(): string;
|
||||||
|
getInstanceBaseUrl(): string;
|
||||||
|
|
||||||
getMode?: () => WorkflowExecuteMode;
|
getMode?: () => WorkflowExecuteMode;
|
||||||
getActivationMode?: () => WorkflowActivateMode;
|
getActivationMode?: () => WorkflowActivateMode;
|
||||||
|
@ -1736,6 +1737,7 @@ export interface IWorkflowExecuteAdditionalData {
|
||||||
httpResponse?: express.Response;
|
httpResponse?: express.Response;
|
||||||
httpRequest?: express.Request;
|
httpRequest?: express.Request;
|
||||||
restApiUrl: string;
|
restApiUrl: string;
|
||||||
|
instanceBaseUrl: string;
|
||||||
setExecutionStatus?: (status: ExecutionStatus) => void;
|
setExecutionStatus?: (status: ExecutionStatus) => void;
|
||||||
sendMessageToUI?: (source: string, message: any) => void;
|
sendMessageToUI?: (source: string, message: any) => void;
|
||||||
timezone: string;
|
timezone: string;
|
||||||
|
|
Loading…
Reference in a new issue