mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 04:34:06 -08:00
Merge branch 'master' of https://github.com/n8n-io/n8n
This commit is contained in:
commit
3a61ad5997
5
.github/workflows/docker-images-rpi.yml
vendored
5
.github/workflows/docker-images-rpi.yml
vendored
|
@ -19,9 +19,10 @@ jobs:
|
|||
run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
uses: crazy-max/ghaction-docker-buildx@v3
|
||||
with:
|
||||
version: latest
|
||||
buildx-version: latest
|
||||
qemu-version: latest
|
||||
- name: Run Buildx (push image)
|
||||
if: success()
|
||||
run: |
|
||||
|
|
|
@ -2,6 +2,26 @@
|
|||
|
||||
This list shows all the versions which include breaking changes and how to upgrade.
|
||||
|
||||
## 0.79.0
|
||||
|
||||
### What changed?
|
||||
|
||||
We have renamed the operations in the Todoist Node for consistency with the codebase. We also deleted the `close_match` and `delete_match` operations as these can be accomplished using the following operations: `getAll`, `close`, and `delete`.
|
||||
|
||||
### When is action necessary?
|
||||
|
||||
When one of the following operations is used:
|
||||
|
||||
- close_by
|
||||
- close_match
|
||||
- delete_id
|
||||
- delete_match
|
||||
|
||||
### How to upgrade:
|
||||
|
||||
After upgrading, open all workflows which contain the Todoist Node. Set the corresponding operation, and then save the workflow.
|
||||
|
||||
If the operations `close_match` or `delete_match` are used, recreate them using the operations: `getAll`, `delete`, and `close`.
|
||||
|
||||
## 0.69.0
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n",
|
||||
"version": "0.78.0",
|
||||
"version": "0.79.0",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -101,8 +101,8 @@
|
|||
"mongodb": "^3.5.5",
|
||||
"mysql2": "^2.0.1",
|
||||
"n8n-core": "~0.43.0",
|
||||
"n8n-editor-ui": "~0.54.0",
|
||||
"n8n-nodes-base": "~0.73.0",
|
||||
"n8n-editor-ui": "~0.55.0",
|
||||
"n8n-nodes-base": "~0.74.0",
|
||||
"n8n-workflow": "~0.39.0",
|
||||
"oauth-1.0a": "^2.2.6",
|
||||
"open": "^7.0.0",
|
||||
|
|
|
@ -213,64 +213,65 @@ class App {
|
|||
const jwtAuthHeader = await GenericHelpers.getConfigValue('security.jwtAuth.jwtHeader') as string;
|
||||
if (jwtAuthHeader === '') {
|
||||
throw new Error('JWT auth is activated but no request header was defined. Please set one!');
|
||||
}
|
||||
}
|
||||
const jwksUri = await GenericHelpers.getConfigValue('security.jwtAuth.jwksUri') as string;
|
||||
if (jwksUri === '') {
|
||||
throw new Error('JWT auth is activated but no JWK Set URI was defined. Please set one!');
|
||||
}
|
||||
const jwtHeaderValuePrefix = await GenericHelpers.getConfigValue('security.jwtAuth.jwtHeaderValuePrefix') as string;
|
||||
const jwtIssuer = await GenericHelpers.getConfigValue('security.jwtAuth.jwtIssuer') as string;
|
||||
const jwtNamespace = await GenericHelpers.getConfigValue('security.jwtAuth.jwtNamespace') as string;
|
||||
const jwtAllowedTenantKey = await GenericHelpers.getConfigValue('security.jwtAuth.jwtAllowedTenantKey') as string;
|
||||
const jwtAllowedTenant = await GenericHelpers.getConfigValue('security.jwtAuth.jwtAllowedTenant') as string;
|
||||
}
|
||||
const jwtHeaderValuePrefix = await GenericHelpers.getConfigValue('security.jwtAuth.jwtHeaderValuePrefix') as string;
|
||||
const jwtIssuer = await GenericHelpers.getConfigValue('security.jwtAuth.jwtIssuer') as string;
|
||||
const jwtNamespace = await GenericHelpers.getConfigValue('security.jwtAuth.jwtNamespace') as string;
|
||||
const jwtAllowedTenantKey = await GenericHelpers.getConfigValue('security.jwtAuth.jwtAllowedTenantKey') as string;
|
||||
const jwtAllowedTenant = await GenericHelpers.getConfigValue('security.jwtAuth.jwtAllowedTenant') as string;
|
||||
|
||||
function isTenantAllowed(decodedToken: object): Boolean {
|
||||
if (jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '') return true;
|
||||
else {
|
||||
for (let [k, v] of Object.entries(decodedToken)) {
|
||||
if (k === jwtNamespace) {
|
||||
for (let [kn, kv] of Object.entries(v)) {
|
||||
if (kn === jwtAllowedTenantKey && kv === jwtAllowedTenant) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function isTenantAllowed(decodedToken: object): boolean {
|
||||
if (jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '') return true;
|
||||
else {
|
||||
for (const [k, v] of Object.entries(decodedToken)) {
|
||||
if (k === jwtNamespace) {
|
||||
for (const [kn, kv] of Object.entries(v)) {
|
||||
if (kn === jwtAllowedTenantKey && kv === jwtAllowedTenant) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (req.url.match(authIgnoreRegex)) {
|
||||
return next();
|
||||
}
|
||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (req.url.match(authIgnoreRegex)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var token = req.header(jwtAuthHeader) as string;
|
||||
if (token === undefined || token === '') {
|
||||
return ResponseHelper.jwtAuthAuthorizationError(res, "Missing token");
|
||||
}
|
||||
if (jwtHeaderValuePrefix != '' && token.startsWith(jwtHeaderValuePrefix)) {
|
||||
token = token.replace(jwtHeaderValuePrefix + ' ', '').trimLeft();
|
||||
}
|
||||
let token = req.header(jwtAuthHeader) as string;
|
||||
if (token === undefined || token === '') {
|
||||
return ResponseHelper.jwtAuthAuthorizationError(res, "Missing token");
|
||||
}
|
||||
if (jwtHeaderValuePrefix != '' && token.startsWith(jwtHeaderValuePrefix)) {
|
||||
token = token.replace(jwtHeaderValuePrefix + ' ', '').trimLeft();
|
||||
}
|
||||
|
||||
const jwkClient = jwks({ cache: true, jwksUri });
|
||||
function getKey(header: any, callback: Function) { // tslint:disable-line:no-any
|
||||
jwkClient.getSigningKey(header.kid, (err: Error, key: any) => { // tslint:disable-line:no-any
|
||||
if (err) throw ResponseHelper.jwtAuthAuthorizationError(res, err.message);
|
||||
const jwkClient = jwks({ cache: true, jwksUri });
|
||||
function getKey(header: any, callback: Function) { // tslint:disable-line:no-any
|
||||
jwkClient.getSigningKey(header.kid, (err: Error, key: any) => { // tslint:disable-line:no-any
|
||||
if (err) throw ResponseHelper.jwtAuthAuthorizationError(res, err.message);
|
||||
|
||||
const signingKey = key.publicKey || key.rsaPublicKey;
|
||||
callback(null, signingKey);
|
||||
});
|
||||
}
|
||||
const signingKey = key.publicKey || key.rsaPublicKey;
|
||||
callback(null, signingKey);
|
||||
});
|
||||
}
|
||||
|
||||
var jwtVerifyOptions: jwt.VerifyOptions = {
|
||||
issuer: jwtIssuer != '' ? jwtIssuer : undefined,
|
||||
ignoreExpiration: false
|
||||
}
|
||||
jwt.verify(token, getKey, jwtVerifyOptions, (err: jwt.VerifyErrors, decoded: object) => {
|
||||
if (err) ResponseHelper.jwtAuthAuthorizationError(res, 'Invalid token');
|
||||
else if (!isTenantAllowed(decoded)) ResponseHelper.jwtAuthAuthorizationError(res, 'Tenant not allowed');
|
||||
else next();
|
||||
const jwtVerifyOptions: jwt.VerifyOptions = {
|
||||
issuer: jwtIssuer !== '' ? jwtIssuer : undefined,
|
||||
ignoreExpiration: false
|
||||
};
|
||||
|
||||
jwt.verify(token, getKey, jwtVerifyOptions, (err: jwt.VerifyErrors, decoded: object) => {
|
||||
if (err) ResponseHelper.jwtAuthAuthorizationError(res, 'Invalid token');
|
||||
else if (!isTenantAllowed(decoded)) ResponseHelper.jwtAuthAuthorizationError(res, 'Tenant not allowed');
|
||||
else next();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -296,6 +297,8 @@ class App {
|
|||
// Make sure that each request has the "parsedUrl" parameter
|
||||
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
(req as ICustomRequest).parsedUrl = parseUrl(req);
|
||||
// @ts-ignore
|
||||
req.rawBody = new Buffer('', 'base64');
|
||||
next();
|
||||
});
|
||||
|
||||
|
@ -309,11 +312,13 @@ class App {
|
|||
|
||||
// Support application/xml type post data
|
||||
// @ts-ignore
|
||||
this.app.use(bodyParser.xml({ limit: '16mb', xmlParseOptions: {
|
||||
normalize: true, // Trim whitespace inside text nodes
|
||||
normalizeTags: true, // Transform tags to lowercase
|
||||
explicitArray: false, // Only put properties in array if length > 1
|
||||
} }));
|
||||
this.app.use(bodyParser.xml({
|
||||
limit: '16mb', xmlParseOptions: {
|
||||
normalize: true, // Trim whitespace inside text nodes
|
||||
normalizeTags: true, // Transform tags to lowercase
|
||||
explicitArray: false, // Only put properties in array if length > 1
|
||||
}
|
||||
}));
|
||||
|
||||
this.app.use(bodyParser.text({
|
||||
limit: '16mb', verify: (req, res, buf) => {
|
||||
|
@ -1008,8 +1013,8 @@ class App {
|
|||
hash_function(base, key) {
|
||||
const algorithm = (signatureMethod === 'HMAC-SHA1') ? 'sha1' : 'sha256';
|
||||
return createHmac(algorithm, key)
|
||||
.update(base)
|
||||
.digest('base64');
|
||||
.update(base)
|
||||
.digest('base64');
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1209,7 +1214,7 @@ class App {
|
|||
|
||||
// Verify and store app code. Generate access tokens and store for respective credential.
|
||||
this.app.get(`/${this.restEndpoint}/oauth2-credential/callback`, async (req: express.Request, res: express.Response) => {
|
||||
const {code, state: stateEncoded } = req.query;
|
||||
const { code, state: stateEncoded } = req.query;
|
||||
|
||||
if (code === undefined || stateEncoded === undefined) {
|
||||
const errorResponse = new ResponseHelper.ResponseError('Insufficient parameters for OAuth2 callback. Received following query parameters: ' + JSON.stringify(req.query), undefined, 503);
|
||||
|
@ -1643,7 +1648,7 @@ class App {
|
|||
response = await this.activeWorkflowRunner.executeWebhook('GET', requestUrl, req, res);
|
||||
} catch (error) {
|
||||
ResponseHelper.sendErrorResponse(res, error);
|
||||
return ;
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.noWebhookResponse === true) {
|
||||
|
@ -1835,13 +1840,13 @@ export async function start(): Promise<void> {
|
|||
|
||||
let server;
|
||||
|
||||
if (app.protocol === 'https' && app.sslKey && app.sslCert){
|
||||
if (app.protocol === 'https' && app.sslKey && app.sslCert) {
|
||||
const https = require('https');
|
||||
const privateKey = readFileSync(app.sslKey, 'utf8');
|
||||
const cert = readFileSync(app.sslCert, 'utf8');
|
||||
const credentials = { key: privateKey,cert };
|
||||
server = https.createServer(credentials,app.app);
|
||||
}else{
|
||||
const credentials = { key: privateKey, cert };
|
||||
server = https.createServer(credentials, app.app);
|
||||
} else {
|
||||
const http = require('http');
|
||||
server = http.createServer(app.app);
|
||||
}
|
||||
|
|
|
@ -50,20 +50,20 @@ export class ActiveWebhooks {
|
|||
// it gets called
|
||||
this.webhookUrls[webhookKey] = webhookData;
|
||||
|
||||
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
|
||||
if (webhookExists === false) {
|
||||
// If webhook does not exist yet create it
|
||||
try {
|
||||
try {
|
||||
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
|
||||
if (webhookExists === false) {
|
||||
// If webhook does not exist yet create it
|
||||
await workflow.runWebhookMethod('create', webhookData, NodeExecuteFunctions, mode, this.testWebhooks);
|
||||
} catch (error) {
|
||||
// If there was a problem unregister the webhook again
|
||||
delete this.webhookUrls[webhookKey];
|
||||
delete this.workflowWebhooks[webhookData.workflowId];
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If there was a problem unregister the webhook again
|
||||
delete this.webhookUrls[webhookKey];
|
||||
delete this.workflowWebhooks[webhookData.workflowId];
|
||||
|
||||
throw error;
|
||||
}
|
||||
this.workflowWebhooks[webhookData.workflowId].push(webhookData);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-editor-ui",
|
||||
"version": "0.54.0",
|
||||
"version": "0.55.0",
|
||||
"description": "Workflow Editor UI for n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
|
|
@ -417,3 +417,5 @@ export interface ITimeoutHMS {
|
|||
minutes: number;
|
||||
seconds: number;
|
||||
}
|
||||
|
||||
export type WorkflowTitleStatus = 'EXECUTING' | 'IDLE' | 'ERROR';
|
||||
|
|
|
@ -1,7 +1,29 @@
|
|||
<template>
|
||||
<div v-if="dialogVisible" @keydown.stop>
|
||||
<el-dialog :visible="dialogVisible" append-to-body width="55%" :title="title" :before-close="closeDialog">
|
||||
|
||||
<el-dialog :visible="dialogVisible" append-to-body width="75%" class="credentials-edit-wrapper" :title="title" :nodeType="nodeType" :before-close="closeDialog">
|
||||
<div name="title" class="title-container" slot="title">
|
||||
<div class="title-left">{{title}}</div>
|
||||
<div class="title-right">
|
||||
<div v-if="credentialType" class="docs-container">
|
||||
<svg class="help-logo" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Node Documentation</title>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-1127.000000, -836.000000)" fill-rule="nonzero">
|
||||
<g transform="translate(1117.000000, 825.000000)">
|
||||
<g transform="translate(10.000000, 11.000000)">
|
||||
<g transform="translate(2.250000, 2.250000)" fill="#FF6150">
|
||||
<path d="M6,11.25 L7.5,11.25 L7.5,9.75 L6,9.75 L6,11.25 M6.75,2.25 C5.09314575,2.25 3.75,3.59314575 3.75,5.25 L5.25,5.25 C5.25,4.42157288 5.92157288,3.75 6.75,3.75 C7.57842712,3.75 8.25,4.42157288 8.25,5.25 C8.25,6.75 6,6.5625 6,9 L7.5,9 C7.5,7.3125 9.75,7.125 9.75,5.25 C9.75,3.59314575 8.40685425,2.25 6.75,2.25 M1.5,0 L12,0 C12.8284271,0 13.5,0.671572875 13.5,1.5 L13.5,12 C13.5,12.8284271 12.8284271,13.5 12,13.5 L1.5,13.5 C0.671572875,13.5 0,12.8284271 0,12 L0,1.5 C0,0.671572875 0.671572875,0 1.5,0 Z"></path>
|
||||
</g>
|
||||
<rect x="0" y="0" width="18" height="18"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<span v-if="credentialType" class="doc-link-text">Need help? <a class="doc-hyperlink" :href="'https://docs.n8n.io/credentials/' + documentationUrl + '/?utm_source=n8n_app&utm_medium=left_nav_menu&utm_campaign=create_new_credentials_modal'" target="_blank">Open credential docs</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="credential-type-item">
|
||||
<el-row v-if="!setCredentialType">
|
||||
<el-col :span="6">
|
||||
|
@ -40,9 +62,11 @@ import {
|
|||
NodeHelpers,
|
||||
ICredentialType,
|
||||
INodeProperties,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { INodeUi } from '../Interface';
|
||||
|
||||
export default mixins(
|
||||
restApi,
|
||||
|
@ -85,6 +109,39 @@ export default mixins(
|
|||
}
|
||||
}
|
||||
},
|
||||
documentationUrl (): string {
|
||||
if (this.editCredentials) {
|
||||
const credentialType = this.$store.getters.credentialType(this.editCredentials.type);
|
||||
if (credentialType.documentationUrl === undefined) {
|
||||
return credentialType.name;
|
||||
} else {
|
||||
return `${credentialType.documentationUrl}`;
|
||||
}
|
||||
} else {
|
||||
if (this.credentialType) {
|
||||
const credentialType = this.$store.getters.credentialType(this.credentialType);
|
||||
|
||||
if (credentialType.documentationUrl === undefined) {
|
||||
return credentialType.name;
|
||||
} else {
|
||||
return `${credentialType.documentationUrl}`;
|
||||
}
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
},
|
||||
node (): INodeUi {
|
||||
return this.$store.getters.activeNode;
|
||||
},
|
||||
nodeType (): INodeTypeDescription | null {
|
||||
const activeNode = this.node;
|
||||
if (this.node) {
|
||||
return this.$store.getters.nodeType(this.node.type);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async dialogVisible (newValue, oldValue): Promise<void> {
|
||||
|
@ -214,7 +271,7 @@ export default mixins(
|
|||
|
||||
this.$showMessage({
|
||||
title: 'Credentials created',
|
||||
message: `The credential "${eventData.data.name}" got created!`,
|
||||
message: `"${eventData.data.name}" credentials were successfully created!`,
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
@ -227,7 +284,7 @@ export default mixins(
|
|||
|
||||
this.$showMessage({
|
||||
title: 'Credentials updated',
|
||||
message: `The credential "${eventData.data.name}" got updated!`,
|
||||
message: `"${eventData.data.name}" credentials were successfully updated!`,
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
@ -245,9 +302,79 @@ export default mixins(
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.credentials-edit-wrapper {
|
||||
.credential-type-item {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
|
||||
.credential-type-item {
|
||||
padding-bottom: 1em;
|
||||
@media (min-width: 1200px){
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-width: 100%;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.docs-container {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1199px){
|
||||
.title-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
line-height: 17px;
|
||||
}
|
||||
|
||||
.docs-container {
|
||||
margin-top: 10px;
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.title-left {
|
||||
flex: 7;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #7a7a7a;
|
||||
vertical-align:middle;
|
||||
}
|
||||
|
||||
.title-right {
|
||||
vertical-align: middle;
|
||||
flex: 3;
|
||||
font-family: "Open Sans";
|
||||
color: #666666;
|
||||
font-size: 12px;
|
||||
font-weight: 510;
|
||||
letter-spacing: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 40%;
|
||||
}
|
||||
|
||||
.help-logo {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.doc-link-text {
|
||||
margin-left: 2px;
|
||||
float: right;
|
||||
word-break: break-word;
|
||||
flex: 9;
|
||||
}
|
||||
|
||||
.doc-hyperlink,
|
||||
.doc-hyperlink:visited,
|
||||
.doc-hyperlink:focus,
|
||||
.doc-hyperlink:active {
|
||||
text-decoration: none;
|
||||
color: #FF6150;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -44,19 +44,19 @@
|
|||
<el-button title="Connect OAuth Credentials" circle :disabled="true">
|
||||
<font-awesome-icon icon="redo" />
|
||||
</el-button>
|
||||
Not all required credential properties are filled
|
||||
Enter all required properties
|
||||
</span>
|
||||
<span v-else-if="isOAuthConnected === true">
|
||||
<el-button title="Reconnect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" circle>
|
||||
<font-awesome-icon icon="redo" />
|
||||
</el-button>
|
||||
Is connected
|
||||
Connected
|
||||
</span>
|
||||
<span v-else>
|
||||
<el-button title="Connect OAuth Credentials" @click.stop="oAuthCredentialAuthorize()" circle>
|
||||
<font-awesome-icon icon="sign-in-alt" />
|
||||
</el-button>
|
||||
Is NOT connected
|
||||
Not connected
|
||||
</span>
|
||||
|
||||
<div v-if="credentialProperties.length">
|
||||
|
@ -91,7 +91,7 @@
|
|||
|
||||
<div v-if="nodesAccess.length === 0" class="no-nodes-access">
|
||||
<strong>
|
||||
Important!
|
||||
Important
|
||||
</strong><br />
|
||||
Add at least one node which has access to the credentials!
|
||||
</div>
|
||||
|
@ -163,8 +163,8 @@ export default mixins(
|
|||
isMinimized: true,
|
||||
helpTexts: {
|
||||
credentialsData: 'The credentials to set.',
|
||||
credentialsName: 'The name the credentials should be saved as. Use a name<br />which makes it clear to what exactly they give access to.<br />For credentials of an Email account that could be the Email address itself.',
|
||||
nodesWithAccess: 'The nodes which allowed to use this credentials.',
|
||||
credentialsName: 'A recognizable label for the credentials. Descriptive names work <br />best here, so you can easily select it from a list later.',
|
||||
nodesWithAccess: 'Nodes with access to these credentials.',
|
||||
},
|
||||
credentialDataTemp: null as ICredentialsDecryptedResponse | null,
|
||||
nodesAccess: [] as string[],
|
||||
|
@ -256,7 +256,7 @@ export default mixins(
|
|||
|
||||
this.$showMessage({
|
||||
title: 'Copied',
|
||||
message: `The callback URL got copied!`,
|
||||
message: `Callback URL was successfully copied!`,
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
|
@ -401,7 +401,7 @@ export default mixins(
|
|||
|
||||
this.$showMessage({
|
||||
title: 'Connected',
|
||||
message: 'Got connected!',
|
||||
message: 'Connected successfully!',
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ export default mixins(
|
|||
try {
|
||||
this.credentials = JSON.parse(JSON.stringify(this.$store.getters.allCredentials));
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Proble loading credentials', 'There was a problem loading the credentials:');
|
||||
this.$showError(error, 'Problem loading credentials', 'There was a problem loading the credentials:');
|
||||
this.isDataLoading = false;
|
||||
return;
|
||||
}
|
||||
|
@ -138,7 +138,7 @@ export default mixins(
|
|||
},
|
||||
|
||||
async deleteCredential (credential: ICredentialsResponse) {
|
||||
const deleteConfirmed = await this.confirmMessage(`Are you sure that you want to delete the credentials "${credential.name}"?`, 'Delete Credentials?', 'warning', 'Yes, delete!');
|
||||
const deleteConfirmed = await this.confirmMessage(`Are you sure you want to delete "${credential.name}" credentials?`, 'Delete Credentials?', 'warning', 'Yes, delete!');
|
||||
|
||||
if (deleteConfirmed === false) {
|
||||
return;
|
||||
|
|
|
@ -9,16 +9,16 @@
|
|||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="showDocumentHelp && nodeType" class="doc-help-wrapper">
|
||||
<svg id="help-logo" v-if="showDocumentHelp && nodeType" :href="'https://docs.n8n.io/nodes/' + nodeType.name" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<svg id="help-logo" v-if="showDocumentHelp && nodeType" :href="'https://docs.n8n.io/nodes/' + nodeType.name" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Node Documentation</title>
|
||||
<g id="MVP-Onboard-proposal" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Node-modal-(docs-link)" transform="translate(-1127.000000, -836.000000)" fill-rule="nonzero">
|
||||
<g id="Group" transform="translate(1117.000000, 825.000000)">
|
||||
<g id="mdi-help-box" transform="translate(10.000000, 11.000000)">
|
||||
<g id="Icon" transform="translate(2.250000, 2.250000)" fill="#FF6150">
|
||||
<path d="M6,11.25 L7.5,11.25 L7.5,9.75 L6,9.75 L6,11.25 M6.75,2.25 C5.09314575,2.25 3.75,3.59314575 3.75,5.25 L5.25,5.25 C5.25,4.42157288 5.92157288,3.75 6.75,3.75 C7.57842712,3.75 8.25,4.42157288 8.25,5.25 C8.25,6.75 6,6.5625 6,9 L7.5,9 C7.5,7.3125 9.75,7.125 9.75,5.25 C9.75,3.59314575 8.40685425,2.25 6.75,2.25 M1.5,0 L12,0 C12.8284271,0 13.5,0.671572875 13.5,1.5 L13.5,12 C13.5,12.8284271 12.8284271,13.5 12,13.5 L1.5,13.5 C0.671572875,13.5 0,12.8284271 0,12 L0,1.5 C0,0.671572875 0.671572875,0 1.5,0 Z" id="Icon-Shape"></path>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-1127.000000, -836.000000)" fill-rule="nonzero">
|
||||
<g transform="translate(1117.000000, 825.000000)">
|
||||
<g transform="translate(10.000000, 11.000000)">
|
||||
<g transform="translate(2.250000, 2.250000)" fill="#FF6150">
|
||||
<path d="M6,11.25 L7.5,11.25 L7.5,9.75 L6,9.75 L6,11.25 M6.75,2.25 C5.09314575,2.25 3.75,3.59314575 3.75,5.25 L5.25,5.25 C5.25,4.42157288 5.92157288,3.75 6.75,3.75 C7.57842712,3.75 8.25,4.42157288 8.25,5.25 C8.25,6.75 6,6.5625 6,9 L7.5,9 C7.5,7.3125 9.75,7.125 9.75,5.25 C9.75,3.59314575 8.40685425,2.25 6.75,2.25 M1.5,0 L12,0 C12.8284271,0 13.5,0.671572875 13.5,1.5 L13.5,12 C13.5,12.8284271 12.8284271,13.5 12,13.5 L1.5,13.5 C0.671572875,13.5 0,12.8284271 0,12 L0,1.5 C0,0.671572875 0.671572875,0 1.5,0 Z"></path>
|
||||
</g>
|
||||
<rect id="ViewBox" x="0" y="0" width="18" height="18"></rect>
|
||||
<rect x="0" y="0" width="18" height="18"></rect>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
|
|
|
@ -12,14 +12,15 @@
|
|||
<font-awesome-icon icon="check" class="execution-icon success" v-if="executionFinished" title="Execution was successful" />
|
||||
<font-awesome-icon icon="times" class="execution-icon error" v-else title="Execution did fail" />
|
||||
</span>
|
||||
of Workflow
|
||||
of
|
||||
<span class="workflow-name clickable" title="Open Workflow">
|
||||
<span @click="openWorkflow(workflowExecution.workflowId)">"{{workflowName}}"</span>
|
||||
</span>
|
||||
workflow
|
||||
</span>
|
||||
<span index="workflow-name" class="current-workflow" v-if="!isReadOnly">
|
||||
<span v-if="currentWorkflow">Workflow: <span class="workflow-name">{{workflowName}}</span></span>
|
||||
<span v-else class="workflow-not-saved">Workflow not saved!</span>
|
||||
<span v-else class="workflow-not-saved">Workflow was not saved!</span>
|
||||
</span>
|
||||
|
||||
<span class="saving-workflow" v-if="isWorkflowSaving">
|
||||
|
@ -32,9 +33,9 @@
|
|||
<div class="push-connection-lost" v-if="!isPushConnectionActive">
|
||||
<el-tooltip placement="bottom-end" effect="light">
|
||||
<div slot="content">
|
||||
Server connection could not be established.<br />
|
||||
The server is down or there is a connection problem.<br />
|
||||
It will reconnect automatically as soon as the backend can be reached.
|
||||
Cannot connect to server.<br />
|
||||
It is either down or you have a connection issue. <br />
|
||||
It should reconnect automatically once the issue is resolved.
|
||||
</div>
|
||||
<span>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
|
@ -50,9 +51,8 @@
|
|||
<div class="read-only" v-if="isReadOnly">
|
||||
<el-tooltip placement="bottom-end" effect="light">
|
||||
<div slot="content">
|
||||
A past execution gets displayed. For that reason no data<br />
|
||||
can be changed. To make changes or to execute it again open<br />
|
||||
the workflow by clicking on it`s name on the left.
|
||||
You're viewing the log of a previous execution. You cannot<br />
|
||||
make changes since this execution already occured. Make changes<br /> to this workflow by clicking on it`s name on the left.
|
||||
</div>
|
||||
<span>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
|
@ -84,6 +84,7 @@ import { genericHelpers } from '@/components/mixins/genericHelpers';
|
|||
import { pushConnection } from '@/components/mixins/pushConnection';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { titleChange } from '@/components/mixins/titleChange';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
|
@ -95,6 +96,7 @@ export default mixins(
|
|||
pushConnection,
|
||||
restApi,
|
||||
showMessage,
|
||||
titleChange,
|
||||
workflowHelpers,
|
||||
)
|
||||
.extend({
|
||||
|
@ -155,6 +157,7 @@ export default mixins(
|
|||
},
|
||||
methods: {
|
||||
async openWorkflow (workflowId: string) {
|
||||
this.$titleSet(this.workflowName, 'IDLE');
|
||||
// Change to other workflow
|
||||
this.$router.push({
|
||||
name: 'NodeViewExisting',
|
||||
|
@ -162,7 +165,6 @@ export default mixins(
|
|||
});
|
||||
},
|
||||
},
|
||||
|
||||
async mounted () {
|
||||
// Initialize the push connection
|
||||
this.pushConnect();
|
||||
|
|
|
@ -179,6 +179,7 @@ import WorkflowSettings from '@/components/WorkflowSettings.vue';
|
|||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { titleChange } from '@/components/mixins/titleChange';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
import { workflowSave } from '@/components/mixins/workflowSave';
|
||||
import { workflowRun } from '@/components/mixins/workflowRun';
|
||||
|
@ -191,6 +192,7 @@ export default mixins(
|
|||
genericHelpers,
|
||||
restApi,
|
||||
showMessage,
|
||||
titleChange,
|
||||
workflowHelpers,
|
||||
workflowRun,
|
||||
workflowSave,
|
||||
|
@ -417,7 +419,8 @@ export default mixins(
|
|||
this.$showError(error, 'Problem deleting the workflow', 'There was a problem deleting the workflow:');
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset tab title since workflow is deleted.
|
||||
this.$titleReset();
|
||||
this.$showMessage({
|
||||
title: 'Workflow got deleted',
|
||||
message: `The workflow "${this.workflowName}" got deleted!`,
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<div class="node-create-list-wrapper">
|
||||
<div class="node-create-list">
|
||||
<div v-if="filteredNodeTypes.length === 0" class="no-results">
|
||||
No node found which matches active filter!
|
||||
🙃 no nodes matching your search criteria
|
||||
</div>
|
||||
<node-create-item :active="index === activeNodeTypeIndex" :nodeType="nodeType" v-for="(nodeType, index) in filteredNodeTypes" v-bind:key="nodeType.name" @nodeTypeSelected="nodeTypeSelected"></node-create-item>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<display-with-change :key-name="'name'" @valueChanged="valueChanged"></display-with-change>
|
||||
<a v-if="nodeType" :href="'http://n8n.io/nodes/' + nodeType.name" target="_blank" class="node-info">
|
||||
<el-tooltip class="clickable" placement="top" effect="light">
|
||||
<div slot="content" v-html="'<strong>Node Description:</strong><br />' + nodeTypeDescription + '<br /><br /><strong>For more information and usage examples click!</strong>'"></div>
|
||||
<div slot="content" v-html="'<strong>Node Description:</strong><br />' + nodeTypeDescription + '<br /><br /><strong>Click the \'?\' icon to open this node on n8n.io </strong>'"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</el-tooltip>
|
||||
</a>
|
||||
|
@ -22,7 +22,7 @@
|
|||
<node-webhooks :node="node" :nodeType="nodeType" />
|
||||
<parameter-input-list :parameters="parametersNoneSetting" :hideDelete="true" :nodeValues="nodeValues" path="parameters" @valueChanged="valueChanged" />
|
||||
<div v-if="parametersNoneSetting.length === 0">
|
||||
The node does not have any parameters.
|
||||
This node does not have any parameters.
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Settings">
|
||||
|
@ -162,15 +162,15 @@ export default mixins(
|
|||
},
|
||||
default: '',
|
||||
noDataExpression: true,
|
||||
description: 'Notes to save with the node.',
|
||||
description: 'Optional note to save with the node.',
|
||||
},
|
||||
{
|
||||
displayName: 'Notes In Flow',
|
||||
displayName: 'Display note in flow?',
|
||||
name: 'notesInFlow',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
description: 'If activated it will display the above notes in the flow as subtitle.',
|
||||
description: 'If active, the note above will display in the flow as a subtitle.',
|
||||
},
|
||||
{
|
||||
displayName: 'Node Color',
|
||||
|
@ -186,7 +186,7 @@ export default mixins(
|
|||
type: 'boolean',
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
description: 'If activated and the node does not have any data for the first output,<br />it returns an empty item anyway. Be careful setting this on<br />IF-Nodes as it could easily cause an infinite loop.',
|
||||
description: 'If active, the node will return an empty item even if the <br />node returns no data during an initial execution. Be careful setting <br />this on IF-Nodes as it could cause an infinite loop.',
|
||||
},
|
||||
{
|
||||
displayName: 'Execute Once',
|
||||
|
@ -194,7 +194,7 @@ export default mixins(
|
|||
type: 'boolean',
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
description: 'Instead of executing once per item does it only execute once with the data of the first item.',
|
||||
description: 'If active, the node executes only once, with data<br /> from the first item it recieves. ',
|
||||
},
|
||||
{
|
||||
displayName: 'Retry On Fail',
|
||||
|
@ -202,7 +202,7 @@ export default mixins(
|
|||
type: 'boolean',
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
description: 'If activated it will automatically retry the node again multiple times.',
|
||||
description: 'If active, the node tries to execute a failed attempt <br /> multiple times until it succeeds.',
|
||||
},
|
||||
{
|
||||
displayName: 'Max. Tries',
|
||||
|
@ -221,7 +221,7 @@ export default mixins(
|
|||
},
|
||||
},
|
||||
noDataExpression: true,
|
||||
description: 'How often it should try to execute the node before it should fail.',
|
||||
description: 'Number of times Retry On Fail should attempt to execute the node <br />before stopping and returning the execution as failed.',
|
||||
},
|
||||
{
|
||||
displayName: 'Wait Between Tries',
|
||||
|
@ -240,7 +240,7 @@ export default mixins(
|
|||
},
|
||||
},
|
||||
noDataExpression: true,
|
||||
description: 'How long to wait between ties. Value in ms.',
|
||||
description: 'How long to wait between each attempt. Value in ms.',
|
||||
},
|
||||
{
|
||||
displayName: 'Continue On Fail',
|
||||
|
@ -248,7 +248,7 @@ export default mixins(
|
|||
type: 'boolean',
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
description: 'If activated and the node fails the workflow will simply continue running.<br />It will then simply pass through the input data so the workflow has<br />to be set up to handle the case that different data gets returned.',
|
||||
description: 'If active, the workflow continues even if this node\'s <br /execution fails. When this occurs, the node passes along input data from<br />previous nodes - so your workflow should account for unexpected output data.',
|
||||
},
|
||||
] as INodeProperties[],
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ export default mixins(
|
|||
|
||||
this.$showMessage({
|
||||
title: 'Copied',
|
||||
message: `The webhook URL got copied!`,
|
||||
message: `The webhook URL was successfully copied!`,
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
:disabled="workflowRunning"
|
||||
@click.stop="runWorkflow(node.name)"
|
||||
class="execute-node-button"
|
||||
:title="`Executes node ${node.name} and all not already executed nodes before it.`"
|
||||
:title="`Executes this ${node.name} node after executing any previous nodes that have not yet returned data`"
|
||||
>
|
||||
<div class="run-icon-button">
|
||||
<font-awesome-icon v-if="!workflowRunning" icon="play-circle"/>
|
||||
|
@ -72,14 +72,14 @@
|
|||
<span v-else>
|
||||
<div v-if="showData === false" class="to-much-data">
|
||||
<h3>
|
||||
Node contains large amount of data
|
||||
Node returned a large amount of data
|
||||
</h3>
|
||||
|
||||
<div class="text">
|
||||
The node contains {{parseInt(dataSize/1024).toLocaleString()}} KB of data.<br />
|
||||
Displaying it could cause problems!<br />
|
||||
<br />
|
||||
If you decide to display it anyway avoid the JSON view!
|
||||
If you do decide to display it, avoid the JSON view!
|
||||
</div>
|
||||
|
||||
<el-button size="small" @click="displayMode = 'Table';showData = true;">
|
||||
|
@ -162,7 +162,7 @@
|
|||
<div>
|
||||
<strong>No data</strong><br />
|
||||
<br />
|
||||
To display data execute the node first by pressing the execute button above.
|
||||
Data returned by this node will display here<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -34,6 +34,7 @@ import WorkflowActivator from '@/components/WorkflowActivator.vue';
|
|||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { titleChange } from '@/components/mixins/titleChange';
|
||||
import { IWorkflowShortResponse } from '@/Interface';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
@ -42,6 +43,7 @@ export default mixins(
|
|||
genericHelpers,
|
||||
restApi,
|
||||
showMessage,
|
||||
titleChange,
|
||||
).extend({
|
||||
name: 'WorkflowOpen',
|
||||
props: [
|
||||
|
@ -89,6 +91,7 @@ export default mixins(
|
|||
},
|
||||
openWorkflow (data: IWorkflowShortResponse, column: any) { // tslint:disable-line:no-any
|
||||
if (column.label !== 'Active') {
|
||||
this.$titleSet(data.name, 'IDLE');
|
||||
this.$emit('openWorkflow', data.id);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -10,12 +10,14 @@ import {
|
|||
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { titleChange } from '@/components/mixins/titleChange';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export const pushConnection = mixins(
|
||||
nodeHelpers,
|
||||
showMessage,
|
||||
titleChange,
|
||||
)
|
||||
.extend({
|
||||
data () {
|
||||
|
@ -147,7 +149,6 @@ export const pushConnection = mixins(
|
|||
*/
|
||||
pushMessageReceived (event: Event, isRetry?: boolean): boolean {
|
||||
const retryAttempts = 5;
|
||||
|
||||
let receivedData: IPushData;
|
||||
try {
|
||||
// @ts-ignore
|
||||
|
@ -201,13 +202,15 @@ export const pushConnection = mixins(
|
|||
|
||||
const runDataExecuted = pushData.data;
|
||||
|
||||
// @ts-ignore
|
||||
const workflow = this.getWorkflow();
|
||||
if (runDataExecuted.finished !== true) {
|
||||
// There was a problem with executing the workflow
|
||||
let errorMessage = 'There was a problem executing the workflow!';
|
||||
if (runDataExecuted.data.resultData.error && runDataExecuted.data.resultData.error.message) {
|
||||
errorMessage = `There was a problem executing the workflow:<br /><strong>"${runDataExecuted.data.resultData.error.message}"</strong>`;
|
||||
}
|
||||
|
||||
this.$titleSet(workflow.name, 'ERROR');
|
||||
this.$showMessage({
|
||||
title: 'Problem executing workflow',
|
||||
message: errorMessage,
|
||||
|
@ -215,6 +218,7 @@ export const pushConnection = mixins(
|
|||
});
|
||||
} else {
|
||||
// Workflow did execute without a problem
|
||||
this.$titleSet(workflow.name, 'IDLE');
|
||||
this.$showMessage({
|
||||
title: 'Workflow got executed',
|
||||
message: 'Workflow did get executed successfully!',
|
||||
|
|
31
packages/editor-ui/src/components/mixins/titleChange.ts
Normal file
31
packages/editor-ui/src/components/mixins/titleChange.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import {
|
||||
WorkflowTitleStatus,
|
||||
} from '../../Interface';
|
||||
|
||||
export const titleChange = Vue.extend({
|
||||
methods: {
|
||||
/**
|
||||
* Change title of n8n tab
|
||||
*
|
||||
* @param {string} workflow Name of workflow
|
||||
* @param {WorkflowTitleStatus} status Status of workflow
|
||||
*/
|
||||
$titleSet(workflow: string, status: WorkflowTitleStatus) {
|
||||
let icon = '⚠️';
|
||||
if (status === 'EXECUTING') {
|
||||
icon = '🔄';
|
||||
} else if (status === 'IDLE') {
|
||||
icon = '▶️';
|
||||
}
|
||||
|
||||
window.document.title = `n8n - ${icon} ${workflow}`;
|
||||
},
|
||||
|
||||
$titleReset() {
|
||||
document.title = `n8n - Workflow Automation`;
|
||||
},
|
||||
|
||||
},
|
||||
});
|
|
@ -14,10 +14,12 @@ import { restApi } from '@/components/mixins/restApi';
|
|||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { titleChange } from './titleChange';
|
||||
|
||||
export const workflowRun = mixins(
|
||||
restApi,
|
||||
workflowHelpers,
|
||||
titleChange,
|
||||
).extend({
|
||||
methods: {
|
||||
// Starts to executes a workflow on server.
|
||||
|
@ -27,6 +29,7 @@ export const workflowRun = mixins(
|
|||
// because then it can not receive the data as it executes.
|
||||
throw new Error('No active connection to server. It is maybe down.');
|
||||
}
|
||||
const workflow = this.getWorkflow();
|
||||
|
||||
this.$store.commit('addActiveAction', 'workflowRunning');
|
||||
|
||||
|
@ -55,6 +58,7 @@ export const workflowRun = mixins(
|
|||
}
|
||||
|
||||
const workflow = this.getWorkflow();
|
||||
this.$titleSet(workflow.name as string, 'EXECUTING');
|
||||
|
||||
try {
|
||||
// Check first if the workflow has any issues before execute it
|
||||
|
@ -78,6 +82,7 @@ export const workflowRun = mixins(
|
|||
type: 'error',
|
||||
duration: 0,
|
||||
});
|
||||
this.$titleSet(workflow.name as string, 'ERROR');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -165,8 +170,9 @@ export const workflowRun = mixins(
|
|||
};
|
||||
this.$store.commit('setWorkflowExecutionData', executionData);
|
||||
|
||||
return await this.runWorkflowApi(startRunData);
|
||||
return await this.runWorkflowApi(startRunData);
|
||||
} catch (error) {
|
||||
this.$titleSet(workflow.name as string, 'ERROR');
|
||||
this.$showError(error, 'Problem running workflow', 'There was a problem running the workflow:');
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -115,6 +115,8 @@ import { mouseSelect } from '@/components/mixins/mouseSelect';
|
|||
import { moveNodeWorkflow } from '@/components/mixins/moveNodeWorkflow';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { titleChange } from '@/components/mixins/titleChange';
|
||||
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
import { workflowRun } from '@/components/mixins/workflowRun';
|
||||
|
||||
|
@ -165,6 +167,7 @@ export default mixins(
|
|||
moveNodeWorkflow,
|
||||
restApi,
|
||||
showMessage,
|
||||
titleChange,
|
||||
workflowHelpers,
|
||||
workflowRun,
|
||||
)
|
||||
|
@ -1324,6 +1327,8 @@ export default mixins(
|
|||
}
|
||||
|
||||
if (workflowId !== null) {
|
||||
const workflow = await this.restApi().getWorkflow(workflowId);
|
||||
this.$titleSet(workflow.name, 'IDLE');
|
||||
// Open existing workflow
|
||||
await this.openWorkflow(workflowId);
|
||||
} else {
|
||||
|
|
21
packages/nodes-base/credentials/ConvertKitApi.credentials.ts
Normal file
21
packages/nodes-base/credentials/ConvertKitApi.credentials.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
|
||||
export class ConvertKitApi implements ICredentialType {
|
||||
name = 'convertKitApi';
|
||||
displayName = 'ConvertKit API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'API Secret',
|
||||
name: 'apiSecret',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
default: '',
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
|
@ -19,6 +19,7 @@ export class GmailOAuth2Api implements ICredentialType {
|
|||
'googleOAuth2Api',
|
||||
];
|
||||
displayName = 'Gmail OAuth2 API';
|
||||
documentationUrl = 'google';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Scope',
|
||||
|
|
|
@ -39,7 +39,7 @@ export class Postgres implements ICredentialType {
|
|||
{
|
||||
displayName: 'Ignore SSL Issues',
|
||||
name: 'allowUnauthorizedCerts',
|
||||
type: 'boolean',
|
||||
type: 'boolean' as NodePropertyTypes,
|
||||
default: false,
|
||||
description: 'Connect even if SSL certificate validation is not possible.',
|
||||
},
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import {
|
||||
ICredentialType,
|
||||
NodePropertyTypes,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class TodoistOAuth2Api implements ICredentialType {
|
||||
name = 'todoistOAuth2Api';
|
||||
extends = [
|
||||
'oAuth2Api',
|
||||
];
|
||||
displayName = 'Todoist OAuth2 API';
|
||||
properties = [
|
||||
{
|
||||
displayName: 'Authorization URL',
|
||||
name: 'authUrl',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'https://todoist.com/oauth/authorize',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Access Token URL',
|
||||
name: 'accessTokenUrl',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'https://todoist.com/oauth/access_token',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Scope',
|
||||
name: 'scope',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'data:read_write,data:delete',
|
||||
},
|
||||
{
|
||||
displayName: 'Auth URI Query Parameters',
|
||||
name: 'authQueryParameters',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'hidden' as NodePropertyTypes,
|
||||
default: 'body',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -13,7 +13,8 @@ export class ZendeskApi implements ICredentialType {
|
|||
name: 'subdomain',
|
||||
type: 'string' as NodePropertyTypes,
|
||||
description: 'The subdomain of your Zendesk work environment.',
|
||||
default: 'n8n',
|
||||
placeholder: 'company',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
|
|
|
@ -237,10 +237,6 @@ export class Coda implements INodeType {
|
|||
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||
const endpoint = `/docs/${docId}/tables/${tableId}/rows`;
|
||||
|
||||
if (options.keyColumns) {
|
||||
// @ts-ignore
|
||||
items[i].json['keyColumns'] = options.keyColumns.split(',') as string[];
|
||||
}
|
||||
if (options.disableParsing) {
|
||||
qs.disableParsing = options.disableParsing as boolean;
|
||||
}
|
||||
|
@ -264,6 +260,11 @@ export class Coda implements INodeType {
|
|||
};
|
||||
}
|
||||
((sendData[endpoint]! as IDataObject).rows! as IDataObject[]).push({ cells });
|
||||
|
||||
if (options.keyColumns) {
|
||||
// @ts-ignore
|
||||
(sendData[endpoint]! as IDataObject).keyColumns! = options.keyColumns.split(',') as string[];
|
||||
}
|
||||
}
|
||||
|
||||
// Now that all data got collected make all the requests
|
||||
|
|
486
packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts
Normal file
486
packages/nodes-base/nodes/ConvertKit/ConvertKit.node.ts
Normal file
|
@ -0,0 +1,486 @@
|
|||
import {
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeTypeDescription,
|
||||
INodeType,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
convertKitApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
import {
|
||||
customFieldFields,
|
||||
customFieldOperations,
|
||||
} from './CustomFieldDescription';
|
||||
|
||||
import {
|
||||
formFields,
|
||||
formOperations,
|
||||
} from './FormDescription';
|
||||
|
||||
import {
|
||||
sequenceFields,
|
||||
sequenceOperations,
|
||||
} from './SequenceDescription';
|
||||
|
||||
import {
|
||||
tagFields,
|
||||
tagOperations,
|
||||
} from './TagDescription';
|
||||
|
||||
import {
|
||||
tagSubscriberFields,
|
||||
tagSubscriberOperations,
|
||||
} from './TagSubscriberDescription';
|
||||
|
||||
export class ConvertKit implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'ConvertKit',
|
||||
name: 'convertKit',
|
||||
icon: 'file:convertKit.png',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume ConvertKit API.',
|
||||
defaults: {
|
||||
name: 'ConvertKit',
|
||||
color: '#fb6970',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'convertKitApi',
|
||||
required: true,
|
||||
}
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Custom Field',
|
||||
value: 'customField',
|
||||
},
|
||||
{
|
||||
name: 'Form',
|
||||
value: 'form',
|
||||
},
|
||||
{
|
||||
name: 'Sequence',
|
||||
value: 'sequence',
|
||||
},
|
||||
{
|
||||
name: 'Tag',
|
||||
value: 'tag',
|
||||
},
|
||||
{
|
||||
name: 'Tag Subscriber',
|
||||
value: 'tagSubscriber',
|
||||
},
|
||||
],
|
||||
default: 'form',
|
||||
description: 'The resource to operate on.'
|
||||
},
|
||||
//--------------------
|
||||
// Field Description
|
||||
//--------------------
|
||||
...customFieldOperations,
|
||||
...customFieldFields,
|
||||
//--------------------
|
||||
// FormDescription
|
||||
//--------------------
|
||||
...formOperations,
|
||||
...formFields,
|
||||
//--------------------
|
||||
// Sequence Description
|
||||
//--------------------
|
||||
...sequenceOperations,
|
||||
...sequenceFields,
|
||||
//--------------------
|
||||
// Tag Description
|
||||
//--------------------
|
||||
...tagOperations,
|
||||
...tagFields,
|
||||
//--------------------
|
||||
// Tag Subscriber Description
|
||||
//--------------------
|
||||
...tagSubscriberOperations,
|
||||
...tagSubscriberFields,
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
// Get all the tags to display them to user so that he can
|
||||
// select them easily
|
||||
async getTags(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { tags } = await convertKitApiRequest.call(this, 'GET', '/tags');
|
||||
for (const tag of tags) {
|
||||
const tagName = tag.name;
|
||||
const tagId = tag.id;
|
||||
returnData.push({
|
||||
name: tagName,
|
||||
value: tagId,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
// Get all the forms to display them to user so that he can
|
||||
// select them easily
|
||||
async getForms(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { forms } = await convertKitApiRequest.call(this, 'GET', '/forms');
|
||||
for (const form of forms) {
|
||||
const formName = form.name;
|
||||
const formId = form.id;
|
||||
returnData.push({
|
||||
name: formName,
|
||||
value: formId,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the sequences to display them to user so that he can
|
||||
// select them easily
|
||||
async getSequences(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { courses } = await convertKitApiRequest.call(this, 'GET', '/sequences');
|
||||
for (const course of courses) {
|
||||
const courseName = course.name;
|
||||
const courseId = course.id;
|
||||
returnData.push({
|
||||
name: courseName,
|
||||
value: courseId,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
const qs: IDataObject = {};
|
||||
let responseData;
|
||||
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
|
||||
if (resource === 'customField') {
|
||||
if (operation === 'create') {
|
||||
|
||||
const label = this.getNodeParameter('label', i) as string;
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'POST', '/custom_fields', { label }, qs);
|
||||
}
|
||||
if (operation === 'delete') {
|
||||
|
||||
const id = this.getNodeParameter('id', i) as string;
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'DELETE', `/custom_fields/${id}`);
|
||||
}
|
||||
if (operation === 'get') {
|
||||
|
||||
const id = this.getNodeParameter('id', i) as string;
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'GET', `/custom_fields/${id}`);
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'GET', `/custom_fields`);
|
||||
|
||||
responseData = responseData.custom_fields;
|
||||
|
||||
if (!returnAll) {
|
||||
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
responseData = responseData.slice(0, limit);
|
||||
}
|
||||
}
|
||||
if (operation === 'update') {
|
||||
|
||||
const id = this.getNodeParameter('id', i) as string;
|
||||
|
||||
const label = this.getNodeParameter('label', i) as string;
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'PUT', `/custom_fields/${id}`, { label });
|
||||
|
||||
responseData = { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
if (resource === 'form') {
|
||||
if (operation === 'addSubscriber') {
|
||||
|
||||
const email = this.getNodeParameter('email', i) as string;
|
||||
|
||||
const formId = this.getNodeParameter('id', i) as string;
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
|
||||
const body: IDataObject = {
|
||||
email,
|
||||
};
|
||||
|
||||
if (additionalFields.firstName) {
|
||||
body.first_name = additionalFields.firstName as string;
|
||||
}
|
||||
|
||||
if (additionalFields.tags) {
|
||||
body.tags = additionalFields.tags as string[];
|
||||
}
|
||||
|
||||
if (additionalFields.fieldsUi) {
|
||||
const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[];
|
||||
if (fieldValues) {
|
||||
body.fields = {};
|
||||
for (const fieldValue of fieldValues) {
|
||||
//@ts-ignore
|
||||
body.fields[fieldValue.key] = fieldValue.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { subscription } = await convertKitApiRequest.call(this, 'POST', `/forms/${formId}/subscribe`, body);
|
||||
|
||||
responseData = subscription;
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'GET', `/forms`);
|
||||
|
||||
responseData = responseData.forms;
|
||||
|
||||
if (!returnAll) {
|
||||
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
responseData = responseData.slice(0, limit);
|
||||
}
|
||||
}
|
||||
if (operation === 'getSubscriptions') {
|
||||
|
||||
const formId = this.getNodeParameter('id', i) as string;
|
||||
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
|
||||
if (additionalFields.subscriberState) {
|
||||
qs.subscriber_state = additionalFields.subscriberState as string;
|
||||
}
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'GET', `/forms/${formId}/subscriptions`, {}, qs);
|
||||
|
||||
responseData = responseData.subscriptions;
|
||||
|
||||
if (!returnAll) {
|
||||
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
responseData = responseData.slice(0, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resource === 'sequence') {
|
||||
if (operation === 'addSubscriber') {
|
||||
|
||||
const email = this.getNodeParameter('email', i) as string;
|
||||
|
||||
const sequenceId = this.getNodeParameter('id', i) as string;
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
|
||||
const body: IDataObject = {
|
||||
email,
|
||||
};
|
||||
|
||||
if (additionalFields.firstName) {
|
||||
body.first_name = additionalFields.firstName as string;
|
||||
}
|
||||
|
||||
if (additionalFields.tags) {
|
||||
body.tags = additionalFields.tags as string[];
|
||||
}
|
||||
|
||||
if (additionalFields.fieldsUi) {
|
||||
const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[];
|
||||
if (fieldValues) {
|
||||
body.fields = {};
|
||||
for (const fieldValue of fieldValues) {
|
||||
//@ts-ignore
|
||||
body.fields[fieldValue.key] = fieldValue.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { subscription } = await convertKitApiRequest.call(this, 'POST', `/sequences/${sequenceId}/subscribe`, body);
|
||||
|
||||
responseData = subscription;
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'GET', `/sequences`);
|
||||
|
||||
responseData = responseData.courses;
|
||||
|
||||
if (!returnAll) {
|
||||
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
responseData = responseData.slice(0, limit);
|
||||
}
|
||||
}
|
||||
if (operation === 'getSubscriptions') {
|
||||
|
||||
const sequenceId = this.getNodeParameter('id', i) as string;
|
||||
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
|
||||
if (additionalFields.subscriberState) {
|
||||
qs.subscriber_state = additionalFields.subscriberState as string;
|
||||
}
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'GET', `/sequences/${sequenceId}/subscriptions`, {}, qs);
|
||||
|
||||
responseData = responseData.subscriptions;
|
||||
|
||||
if (!returnAll) {
|
||||
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
responseData = responseData.slice(0, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resource === 'tag') {
|
||||
if (operation === 'create') {
|
||||
|
||||
const names = ((this.getNodeParameter('name', i) as string).split(',') as string[]).map((e) => ({ name: e }));
|
||||
|
||||
const body: IDataObject = {
|
||||
tag: names
|
||||
};
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'POST', '/tags', body);
|
||||
}
|
||||
|
||||
if (operation === 'getAll') {
|
||||
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'GET', `/tags`);
|
||||
|
||||
responseData = responseData.tags;
|
||||
|
||||
if (!returnAll) {
|
||||
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
responseData = responseData.slice(0, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resource === 'tagSubscriber') {
|
||||
|
||||
if (operation === 'add') {
|
||||
|
||||
const tagId = this.getNodeParameter('tagId', i) as string;
|
||||
|
||||
const email = this.getNodeParameter('email', i) as string;
|
||||
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
|
||||
const body: IDataObject = {
|
||||
email,
|
||||
};
|
||||
|
||||
if (additionalFields.firstName) {
|
||||
body.first_name = additionalFields.firstName as string;
|
||||
}
|
||||
|
||||
if (additionalFields.fieldsUi) {
|
||||
const fieldValues = (additionalFields.fieldsUi as IDataObject).fieldsValues as IDataObject[];
|
||||
if (fieldValues) {
|
||||
body.fields = {};
|
||||
for (const fieldValue of fieldValues) {
|
||||
//@ts-ignore
|
||||
body.fields[fieldValue.key] = fieldValue.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { subscription } = await convertKitApiRequest.call(this, 'POST', `/tags/${tagId}/subscribe`, body);
|
||||
|
||||
responseData = subscription;
|
||||
}
|
||||
|
||||
if (operation === 'getAll') {
|
||||
|
||||
const tagId = this.getNodeParameter('tagId', i) as string;
|
||||
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'GET', `/tags/${tagId}/subscriptions`);
|
||||
|
||||
responseData = responseData.subscriptions;
|
||||
|
||||
if (!returnAll) {
|
||||
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
|
||||
responseData = responseData.slice(0, limit);
|
||||
}
|
||||
}
|
||||
|
||||
if (operation === 'delete') {
|
||||
|
||||
const tagId = this.getNodeParameter('tagId', i) as string;
|
||||
|
||||
const email = this.getNodeParameter('email', i) as string;
|
||||
|
||||
responseData = await convertKitApiRequest.call(this, 'POST', `/tags/${tagId}>/unsubscribe`, { email });
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else if (responseData !== undefined) {
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
367
packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts
Normal file
367
packages/nodes-base/nodes/ConvertKit/ConvertKitTrigger.node.ts
Normal file
|
@ -0,0 +1,367 @@
|
|||
import {
|
||||
IHookFunctions,
|
||||
IWebhookFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodePropertyOptions,
|
||||
INodeTypeDescription,
|
||||
INodeType,
|
||||
IWebhookResponseData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
convertKitApiRequest,
|
||||
} from './GenericFunctions';
|
||||
|
||||
import {
|
||||
snakeCase,
|
||||
} from 'change-case';
|
||||
|
||||
export class ConvertKitTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'ConvertKit Trigger',
|
||||
name: 'convertKitTrigger',
|
||||
icon: 'file:convertKit.png',
|
||||
subtitle: '={{$parameter["event"]}}',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Handle ConvertKit events via webhooks',
|
||||
defaults: {
|
||||
name: 'ConvertKit Trigger',
|
||||
color: '#fb6970',
|
||||
},
|
||||
inputs: [],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'convertKitApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
webhooks: [
|
||||
{
|
||||
name: 'default',
|
||||
httpMethod: 'POST',
|
||||
responseMode: 'onReceived',
|
||||
path: 'webhook',
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Event',
|
||||
name: 'event',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'The events that can trigger the webhook and whether they are enabled.',
|
||||
options: [
|
||||
{
|
||||
name: 'Form Subscribe',
|
||||
value: 'formSubscribe',
|
||||
},
|
||||
{
|
||||
name: 'Link Click',
|
||||
value: 'linkClick',
|
||||
},
|
||||
{
|
||||
name: 'Product Purchase',
|
||||
value: 'productPurchase',
|
||||
},
|
||||
{
|
||||
name: 'Purchase Created',
|
||||
value: 'purchaseCreate',
|
||||
},
|
||||
{
|
||||
name: 'Sequence Complete',
|
||||
value: 'courseComplete',
|
||||
},
|
||||
{
|
||||
name: 'Sequence Subscribe',
|
||||
value: 'courseSubscribe',
|
||||
},
|
||||
{
|
||||
name: 'Subscriber Activated',
|
||||
value: 'subscriberActivate',
|
||||
},
|
||||
{
|
||||
name: 'Subscriber Unsubscribe',
|
||||
value: 'subscriberUnsubscribe',
|
||||
},
|
||||
{
|
||||
name: 'Tag Add',
|
||||
value: 'tagAdd',
|
||||
},
|
||||
{
|
||||
name: 'Tag Remove',
|
||||
value: 'tagRemove',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Form ID',
|
||||
name: 'formId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getForms',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
event: [
|
||||
'formSubscribe',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Sequence ID',
|
||||
name: 'courseId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getSequences',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
event: [
|
||||
'courseSubscribe',
|
||||
'courseComplete',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Initiating Link',
|
||||
name: 'link',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
description: 'The URL of the initiating link',
|
||||
displayOptions: {
|
||||
show: {
|
||||
event: [
|
||||
'linkClick',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Product ID',
|
||||
name: 'productId',
|
||||
type: 'string',
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
event: [
|
||||
'productPurchase',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tag ID',
|
||||
name: 'tagId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTags',
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
event: [
|
||||
'tagAdd',
|
||||
'tagRemove',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
// Get all the tags to display them to user so that he can
|
||||
// select them easily
|
||||
async getTags(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
|
||||
const { tags } = await convertKitApiRequest.call(this, 'GET', '/tags');
|
||||
|
||||
for (const tag of tags) {
|
||||
|
||||
const tagName = tag.name;
|
||||
|
||||
const tagId = tag.id;
|
||||
|
||||
returnData.push({
|
||||
name: tagName,
|
||||
value: tagId,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
// Get all the forms to display them to user so that he can
|
||||
// select them easily
|
||||
async getForms(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
|
||||
const { forms } = await convertKitApiRequest.call(this, 'GET', '/forms');
|
||||
|
||||
for (const form of forms) {
|
||||
|
||||
const formName = form.name;
|
||||
|
||||
const formId = form.id;
|
||||
|
||||
returnData.push({
|
||||
name: formName,
|
||||
value: formId,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the sequences to display them to user so that he can
|
||||
// select them easily
|
||||
async getSequences(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
|
||||
const { courses } = await convertKitApiRequest.call(this, 'GET', '/sequences');
|
||||
|
||||
for (const course of courses) {
|
||||
|
||||
const courseName = course.name;
|
||||
|
||||
const courseId = course.id;
|
||||
|
||||
returnData.push({
|
||||
name: courseName,
|
||||
value: courseId,
|
||||
});
|
||||
}
|
||||
|
||||
return returnData;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore (because of request)
|
||||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
|
||||
// THe API does not have an endpoint to list all webhooks
|
||||
|
||||
if (webhookData.webhookId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
|
||||
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||
|
||||
let event = this.getNodeParameter('event', 0) as string;
|
||||
|
||||
const endpoint = '/automations/hooks';
|
||||
|
||||
if (event === 'purchaseCreate') {
|
||||
event = `purchase.${snakeCase(event)}`;
|
||||
} else {
|
||||
event = `subscriber.${snakeCase(event)}`;
|
||||
}
|
||||
|
||||
const body: IDataObject = {
|
||||
target_url: webhookUrl as string,
|
||||
event: {
|
||||
name: event
|
||||
},
|
||||
};
|
||||
|
||||
if (event === 'subscriber.form_subscribe') {
|
||||
//@ts-ignore
|
||||
body.event['form_id'] = this.getNodeParameter('formId', 0);
|
||||
}
|
||||
|
||||
if (event === 'subscriber.course_subscribe' || event === 'subscriber.course_complete') {
|
||||
//@ts-ignore
|
||||
body.event['sequence_id'] = this.getNodeParameter('courseId', 0);
|
||||
}
|
||||
|
||||
if (event === 'subscriber.link_click') {
|
||||
//@ts-ignore
|
||||
body.event['initiator_value'] = this.getNodeParameter('link', 0);
|
||||
}
|
||||
|
||||
if (event === 'subscriber.product_purchase') {
|
||||
//@ts-ignore
|
||||
body.event['product_id'] = this.getNodeParameter('productId', 0);
|
||||
}
|
||||
|
||||
if (event === 'subscriber.tag_add' || event === 'subscriber.tag_remove') {
|
||||
//@ts-ignore
|
||||
body.event['tag_id'] = this.getNodeParameter('tagId', 0);
|
||||
}
|
||||
|
||||
const webhook = await convertKitApiRequest.call(this, 'POST', endpoint, body);
|
||||
|
||||
if (webhook.rule.id === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
|
||||
webhookData.webhookId = webhook.rule.id as string;
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
async delete(this: IHookFunctions): Promise<boolean> {
|
||||
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
|
||||
if (webhookData.webhookId !== undefined) {
|
||||
|
||||
const endpoint = `/automations/hooks/${webhookData.webhookId}`;
|
||||
|
||||
try {
|
||||
await convertKitApiRequest.call(this, 'DELETE', endpoint);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
delete webhookData.webhookId;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||
const returnData: IDataObject[] = [];
|
||||
returnData.push(this.getBodyData());
|
||||
|
||||
return {
|
||||
workflowData: [
|
||||
this.helpers.returnJsonArray(returnData),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
124
packages/nodes-base/nodes/ConvertKit/CustomFieldDescription.ts
Normal file
124
packages/nodes-base/nodes/ConvertKit/CustomFieldDescription.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const customFieldOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'customField',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a field',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a field',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all fields',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update a field',
|
||||
},
|
||||
],
|
||||
default: 'update',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const customFieldFields = [
|
||||
{
|
||||
displayName: 'Field ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'customField',
|
||||
],
|
||||
operation: [
|
||||
'update',
|
||||
'delete',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'The ID of your custom field.',
|
||||
},
|
||||
{
|
||||
displayName: 'Label',
|
||||
name: 'label',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'customField',
|
||||
],
|
||||
operation: [
|
||||
'update',
|
||||
'create',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'The label of the custom field.',
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'customField',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'If all results should be returned or only up to a given limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'customField',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 500,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
] as INodeProperties[];
|
220
packages/nodes-base/nodes/ConvertKit/FormDescription.ts
Normal file
220
packages/nodes-base/nodes/ConvertKit/FormDescription.ts
Normal file
|
@ -0,0 +1,220 @@
|
|||
import {
|
||||
INodeProperties
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const formOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'form',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add Subscriber',
|
||||
value: 'addSubscriber',
|
||||
description: 'Add a subscriber',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all forms',
|
||||
},
|
||||
{
|
||||
name: 'Get Subscriptions',
|
||||
value: 'getSubscriptions',
|
||||
description: 'List subscriptions to a form including subscriber data',
|
||||
},
|
||||
],
|
||||
default: 'addSubscriber',
|
||||
description: 'The operations to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const formFields = [
|
||||
{
|
||||
displayName: 'Form ID',
|
||||
name: 'id',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getForms',
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'form',
|
||||
],
|
||||
operation: [
|
||||
'addSubscriber',
|
||||
'getSubscriptions',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Form ID.',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'form',
|
||||
],
|
||||
operation: [
|
||||
'addSubscriber',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: `The subscriber's email address.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'form',
|
||||
],
|
||||
operation: [
|
||||
'addSubscriber',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Custom Fields',
|
||||
name: 'fieldsUi',
|
||||
placeholder: 'Add Custom Field',
|
||||
description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'fieldsValues',
|
||||
displayName: 'Custom Field',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'last_name',
|
||||
description: `The field's key.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Field Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'Doe',
|
||||
description: 'Value of the field.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The subscriber's first name.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
'getSubscriptions',
|
||||
],
|
||||
resource: [
|
||||
'form',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'If all results should be returned or only up to a given limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
'getSubscriptions',
|
||||
],
|
||||
resource: [
|
||||
'form',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 500,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'form',
|
||||
],
|
||||
operation: [
|
||||
'getSubscriptions',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Subscriber State',
|
||||
name: 'subscriberState',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Active',
|
||||
value: 'active',
|
||||
},
|
||||
{
|
||||
name: 'Cancelled',
|
||||
value: 'cancelled',
|
||||
},
|
||||
],
|
||||
default: 'active',
|
||||
},
|
||||
],
|
||||
description: 'Receive only active subscribers or cancelled subscribers.',
|
||||
},
|
||||
] as INodeProperties[];
|
67
packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts
Normal file
67
packages/nodes-base/nodes/ConvertKit/GenericFunctions.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import {
|
||||
OptionsWithUri
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IExecuteSingleFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
IHookFunctions
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function convertKitApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IHookFunctions,
|
||||
method: string, endpoint: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
|
||||
const credentials = this.getCredentials('convertKitApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
let options: OptionsWithUri = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method,
|
||||
qs,
|
||||
body,
|
||||
uri: uri || `https://api.convertkit.com/v3${endpoint}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
options = Object.assign({}, options, option);
|
||||
|
||||
if (Object.keys(options.body).length === 0) {
|
||||
delete options.body;
|
||||
}
|
||||
|
||||
// it's a webhook so include the api secret on the body
|
||||
if ((options.uri as string).includes('/automations/hooks')) {
|
||||
options.body['api_secret'] = credentials.apiSecret;
|
||||
} else {
|
||||
qs.api_secret = credentials.apiSecret;
|
||||
}
|
||||
|
||||
if (Object.keys(options.qs).length === 0) {
|
||||
delete options.qs;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
return await this.helpers.request!(options);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
let errorMessage = error;
|
||||
|
||||
if (error.response && error.response.body && error.response.body.message) {
|
||||
errorMessage = error.response.body.message;
|
||||
}
|
||||
|
||||
throw new Error(`ConvertKit error response: ${errorMessage}`);
|
||||
}
|
||||
}
|
230
packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts
Normal file
230
packages/nodes-base/nodes/ConvertKit/SequenceDescription.ts
Normal file
|
@ -0,0 +1,230 @@
|
|||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const sequenceOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'sequence',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add Subscriber',
|
||||
value: 'addSubscriber',
|
||||
description: 'Add a subscriber',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all sequences',
|
||||
},
|
||||
{
|
||||
name: 'Get Subscriptions',
|
||||
value: 'getSubscriptions',
|
||||
description: 'Get all subscriptions to a sequence including subscriber data',
|
||||
},
|
||||
],
|
||||
default: 'addSubscriber',
|
||||
description: 'The operations to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const sequenceFields = [
|
||||
{
|
||||
displayName: 'Sequence ID',
|
||||
name: 'id',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getSequences',
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'sequence',
|
||||
],
|
||||
operation: [
|
||||
'addSubscriber',
|
||||
'getSubscriptions',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Sequence ID.',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'sequence',
|
||||
],
|
||||
operation: [
|
||||
'addSubscriber',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: `The subscriber's email address.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
'getSubscriptions',
|
||||
],
|
||||
resource: [
|
||||
'sequence',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'If all results should be returned or only up to a given limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
'getSubscriptions',
|
||||
],
|
||||
resource: [
|
||||
'sequence',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 500,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'sequence',
|
||||
],
|
||||
operation: [
|
||||
'addSubscriber',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Custom Fields',
|
||||
name: 'fieldsUi',
|
||||
placeholder: 'Add Custom Field',
|
||||
description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'fieldsValues',
|
||||
displayName: 'Custom Field',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'last_name',
|
||||
description: `The field's key.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Field Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'Doe',
|
||||
description: 'Value of the field.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The subscriber's first name.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Tag IDs',
|
||||
name: 'tags',
|
||||
type: 'multiOptions',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTags',
|
||||
},
|
||||
default: [],
|
||||
description: 'Tags',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'sequence',
|
||||
],
|
||||
operation: [
|
||||
'getSubscriptions',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Subscriber State',
|
||||
name: 'subscriberState',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Active',
|
||||
value: 'active',
|
||||
},
|
||||
{
|
||||
name: 'Cancelled',
|
||||
value: 'cancelled',
|
||||
},
|
||||
],
|
||||
default: 'active',
|
||||
},
|
||||
],
|
||||
description: 'Receive only active subscribers or cancelled subscribers.',
|
||||
},
|
||||
] as INodeProperties[];
|
94
packages/nodes-base/nodes/ConvertKit/TagDescription.ts
Normal file
94
packages/nodes-base/nodes/ConvertKit/TagDescription.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const tagOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'tag',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a tag',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all tags',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const tagFields = [
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'tag',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Tag name, multiple can be added separated by comma',
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'tag',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'If all results should be returned or only up to a given limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'tag',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 500,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
] as INodeProperties[];
|
219
packages/nodes-base/nodes/ConvertKit/TagSubscriberDescription.ts
Normal file
219
packages/nodes-base/nodes/ConvertKit/TagSubscriberDescription.ts
Normal file
|
@ -0,0 +1,219 @@
|
|||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const tagSubscriberOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'tagSubscriber',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Add',
|
||||
value: 'add',
|
||||
description: 'Add a tag to a subscriber',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'List subscriptions to a tag including subscriber data',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a tag from a subscriber',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const tagSubscriberFields = [
|
||||
{
|
||||
displayName: 'Tag ID',
|
||||
name: 'tagId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTags',
|
||||
},
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'tagSubscriber',
|
||||
],
|
||||
operation: [
|
||||
'add',
|
||||
'getAll',
|
||||
'delete',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'tagSubscriber',
|
||||
],
|
||||
operation: [
|
||||
'add',
|
||||
'delete',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
description: 'Subscriber email address.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'tagSubscriber',
|
||||
],
|
||||
operation: [
|
||||
'add',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Custom Fields',
|
||||
name: 'fields',
|
||||
placeholder: 'Add Custom Field',
|
||||
description: 'Object of key/value pairs for custom fields (the custom field must exist before you can use it here).',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'field',
|
||||
displayName: 'Custom Field',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'last_name',
|
||||
description: `The field's key.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Field Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'Doe',
|
||||
description: 'Value of the field.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'First Name',
|
||||
name: 'firstName',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Subscriber first name.',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'tagSubscriber',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'If all results should be returned or only up to a given limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'tagSubscriber',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 500,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'tagSubscriber',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Subscriber State',
|
||||
name: 'subscriberState',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Active',
|
||||
value: 'active',
|
||||
},
|
||||
{
|
||||
name: 'Cancelled',
|
||||
value: 'cancelled',
|
||||
},
|
||||
],
|
||||
default: 'active',
|
||||
},
|
||||
],
|
||||
description: 'Receive only active subscribers or cancelled subscribers.',
|
||||
},
|
||||
] as INodeProperties[];
|
BIN
packages/nodes-base/nodes/ConvertKit/convertKit.png
Normal file
BIN
packages/nodes-base/nodes/ConvertKit/convertKit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
|
@ -1,6 +1,7 @@
|
|||
import { ITriggerFunctions } from 'n8n-core';
|
||||
import {
|
||||
IBinaryData,
|
||||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
|
@ -8,7 +9,13 @@ import {
|
|||
ITriggerResponse,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { connect as imapConnect, ImapSimple, ImapSimpleOptions, getParts, Message } from 'imap-simple';
|
||||
import { connect as imapConnect, ImapSimple, ImapSimpleOptions, getParts, Message } from 'imap-simple';
|
||||
import {
|
||||
simpleParser,
|
||||
Source as ParserSource,
|
||||
} from 'mailparser';
|
||||
|
||||
import * as lodash from 'lodash';
|
||||
|
||||
export class EmailReadImap implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
@ -44,11 +51,11 @@ export class EmailReadImap implements INodeType {
|
|||
options: [
|
||||
{
|
||||
name: 'Mark as read',
|
||||
value: 'read'
|
||||
value: 'read',
|
||||
},
|
||||
{
|
||||
name: 'Nothing',
|
||||
value: 'nothing'
|
||||
value: 'nothing',
|
||||
},
|
||||
],
|
||||
default: 'read',
|
||||
|
@ -59,8 +66,39 @@ export class EmailReadImap implements INodeType {
|
|||
name: 'downloadAttachments',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: {
|
||||
show: {
|
||||
format: [
|
||||
'simple',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'If attachments of emails should be downloaded.<br />Only set if needed as it increases processing.',
|
||||
},
|
||||
{
|
||||
displayName: 'Format',
|
||||
name: 'format',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'RAW',
|
||||
value: 'raw',
|
||||
description: 'Returns the full email message data with body content in the raw field as a base64url encoded string; the payload field is not used.'
|
||||
},
|
||||
{
|
||||
name: 'Resolved',
|
||||
value: 'resolved',
|
||||
description: 'Returns the full email with all data resolved and attachments saved as binary data.',
|
||||
},
|
||||
{
|
||||
name: 'Simple',
|
||||
value: 'simple',
|
||||
description: 'Returns the full email; do not use if you wish to gather inline attachments.',
|
||||
},
|
||||
],
|
||||
default: 'simple',
|
||||
description: 'The format to return the message in',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Prefix Name',
|
||||
name: 'dataPropertyAttachmentsPrefixName',
|
||||
|
@ -68,8 +106,25 @@ export class EmailReadImap implements INodeType {
|
|||
default: 'attachment_',
|
||||
displayOptions: {
|
||||
show: {
|
||||
format: [
|
||||
'resolved',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'Prefix for name of the binary property to which to<br />write the attachments. An index starting with 0 will be added.<br />So if name is "attachment_" the first attachment is saved to "attachment_0"',
|
||||
},
|
||||
{
|
||||
displayName: 'Property Prefix Name',
|
||||
name: 'dataPropertyAttachmentsPrefixName',
|
||||
type: 'string',
|
||||
default: 'attachment_',
|
||||
displayOptions: {
|
||||
show: {
|
||||
format: [
|
||||
'simple',
|
||||
],
|
||||
downloadAttachments: [
|
||||
true
|
||||
true,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -105,7 +160,6 @@ export class EmailReadImap implements INodeType {
|
|||
|
||||
const mailbox = this.getNodeParameter('mailbox') as string;
|
||||
const postProcessAction = this.getNodeParameter('postProcessAction') as string;
|
||||
const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean;
|
||||
const options = this.getNodeParameter('options', {}) as IDataObject;
|
||||
|
||||
|
||||
|
@ -156,16 +210,26 @@ export class EmailReadImap implements INodeType {
|
|||
|
||||
// Returns all the new unseen messages
|
||||
const getNewEmails = async (connection: ImapSimple): Promise<INodeExecutionData[]> => {
|
||||
|
||||
const format = this.getNodeParameter('format', 0) as string;
|
||||
const searchCriteria = [
|
||||
'UNSEEN'
|
||||
];
|
||||
|
||||
const fetchOptions = {
|
||||
bodies: ['HEADER', 'TEXT'],
|
||||
markSeen: postProcessAction === 'read',
|
||||
struct: true,
|
||||
};
|
||||
let fetchOptions = {};
|
||||
|
||||
if (format === 'simple' || format === 'raw') {
|
||||
fetchOptions = {
|
||||
bodies: ['TEXT', 'HEADER'],
|
||||
markSeen: postProcessAction === 'read',
|
||||
struct: true,
|
||||
};
|
||||
} else if (format === 'resolved') {
|
||||
fetchOptions = {
|
||||
bodies: [''],
|
||||
markSeen: postProcessAction === 'read',
|
||||
struct: true,
|
||||
};
|
||||
}
|
||||
|
||||
const results = await connection.search(searchCriteria, fetchOptions);
|
||||
|
||||
|
@ -174,10 +238,7 @@ export class EmailReadImap implements INodeType {
|
|||
let attachments: IBinaryData[];
|
||||
let propertyName: string;
|
||||
|
||||
let dataPropertyAttachmentsPrefixName = '';
|
||||
if (downloadAttachments === true) {
|
||||
dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string;
|
||||
}
|
||||
|
||||
|
||||
// All properties get by default moved to metadata except the ones
|
||||
// which are defined here which get set on the top level.
|
||||
|
@ -188,45 +249,83 @@ export class EmailReadImap implements INodeType {
|
|||
'subject',
|
||||
'to',
|
||||
];
|
||||
for (const message of results) {
|
||||
|
||||
const parts = getParts(message.attributes.struct!);
|
||||
if (format === 'resolved') {
|
||||
const dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string;
|
||||
|
||||
newEmail = {
|
||||
json: {
|
||||
textHtml: await getText(parts, message, 'html'),
|
||||
textPlain: await getText(parts, message, 'plain'),
|
||||
metadata: {} as IDataObject,
|
||||
for (const message of results) {
|
||||
const part = lodash.find(message.parts, { which: '' });
|
||||
|
||||
if (part === undefined) {
|
||||
throw new Error('Email part could not be parsed.');
|
||||
}
|
||||
};
|
||||
const parsedEmail = await parseRawEmail.call(this, part.body, dataPropertyAttachmentsPrefixName);
|
||||
|
||||
messageHeader = message.parts.filter((part) => {
|
||||
return part.which === 'HEADER';
|
||||
});
|
||||
|
||||
messageBody = messageHeader[0].body;
|
||||
for (propertyName of Object.keys(messageBody)) {
|
||||
if (messageBody[propertyName].length) {
|
||||
if (topLevelProperties.includes(propertyName)) {
|
||||
newEmail.json[propertyName] = messageBody[propertyName][0];
|
||||
} else {
|
||||
(newEmail.json.metadata as IDataObject)[propertyName] = messageBody[propertyName][0];
|
||||
}
|
||||
}
|
||||
newEmails.push(parsedEmail);
|
||||
}
|
||||
} else if (format === 'simple') {
|
||||
const downloadAttachments = this.getNodeParameter('downloadAttachments') as boolean;
|
||||
|
||||
let dataPropertyAttachmentsPrefixName = '';
|
||||
if (downloadAttachments === true) {
|
||||
// Get attachments and add them if any get found
|
||||
attachments = await getAttachment(connection, parts, message);
|
||||
if (attachments.length) {
|
||||
newEmail.binary = {};
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
newEmail.binary[`${dataPropertyAttachmentsPrefixName}${i}`] = attachments[i];
|
||||
}
|
||||
}
|
||||
dataPropertyAttachmentsPrefixName = this.getNodeParameter('dataPropertyAttachmentsPrefixName') as string;
|
||||
}
|
||||
|
||||
newEmails.push(newEmail);
|
||||
for (const message of results) {
|
||||
const parts = getParts(message.attributes.struct!);
|
||||
|
||||
newEmail = {
|
||||
json: {
|
||||
textHtml: await getText(parts, message, 'html'),
|
||||
textPlain: await getText(parts, message, 'plain'),
|
||||
metadata: {} as IDataObject,
|
||||
}
|
||||
};
|
||||
|
||||
messageHeader = message.parts.filter((part) => {
|
||||
return part.which === 'HEADER';
|
||||
});
|
||||
|
||||
messageBody = messageHeader[0].body;
|
||||
for (propertyName of Object.keys(messageBody)) {
|
||||
if (messageBody[propertyName].length) {
|
||||
if (topLevelProperties.includes(propertyName)) {
|
||||
newEmail.json[propertyName] = messageBody[propertyName][0];
|
||||
} else {
|
||||
(newEmail.json.metadata as IDataObject)[propertyName] = messageBody[propertyName][0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadAttachments === true) {
|
||||
// Get attachments and add them if any get found
|
||||
attachments = await getAttachment(connection, parts, message);
|
||||
if (attachments.length) {
|
||||
newEmail.binary = {};
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
newEmail.binary[`${dataPropertyAttachmentsPrefixName}${i}`] = attachments[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newEmails.push(newEmail);
|
||||
}
|
||||
} else if (format === 'raw') {
|
||||
for (const message of results) {
|
||||
const part = lodash.find(message.parts, { which: 'TEXT' });
|
||||
|
||||
if (part === undefined) {
|
||||
throw new Error('Email part could not be parsed.');
|
||||
}
|
||||
// Return base64 string
|
||||
newEmail = {
|
||||
json: {
|
||||
raw: part.body
|
||||
}
|
||||
};
|
||||
|
||||
newEmails.push(newEmail);
|
||||
}
|
||||
}
|
||||
|
||||
return newEmails;
|
||||
|
@ -277,3 +376,32 @@ export class EmailReadImap implements INodeType {
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
export async function parseRawEmail(this: ITriggerFunctions, messageEncoded: ParserSource, dataPropertyNameDownload: string): Promise<INodeExecutionData> {
|
||||
const responseData = await simpleParser(messageEncoded);
|
||||
const headers: IDataObject = {};
|
||||
for (const header of responseData.headerLines) {
|
||||
headers[header.key] = header.line;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
responseData.headers = headers;
|
||||
// @ts-ignore
|
||||
responseData.headerLines = undefined;
|
||||
|
||||
const binaryData: IBinaryKeyData = {};
|
||||
if (responseData.attachments) {
|
||||
|
||||
for (let i = 0; i < responseData.attachments.length; i++) {
|
||||
const attachment = responseData.attachments[i];
|
||||
binaryData[`${dataPropertyNameDownload}${i}`] = await this.helpers.prepareBinaryData(attachment.content, attachment.filename, attachment.contentType);
|
||||
}
|
||||
// @ts-ignore
|
||||
responseData.attachments = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
json: responseData as unknown as IDataObject,
|
||||
binary: Object.keys(binaryData).length ? binaryData : undefined,
|
||||
} as INodeExecutionData;
|
||||
}
|
||||
|
|
|
@ -821,7 +821,7 @@ export class HttpRequest implements INodeType {
|
|||
}
|
||||
else if (oAuth2Api !== undefined) {
|
||||
//@ts-ignore
|
||||
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, 'Bearer');
|
||||
response = await this.helpers.requestOAuth2.call(this, 'oAuth2Api', requestOptions, { tokenType: 'Bearer' });
|
||||
} else {
|
||||
response = await this.helpers.request(requestOptions);
|
||||
}
|
||||
|
|
|
@ -171,6 +171,41 @@ export const contactFields = [
|
|||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Properties',
|
||||
name: 'customPropertiesUi',
|
||||
placeholder: 'Add Custom Property',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'customPropertiesValues',
|
||||
displayName: 'Custom Property',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Property',
|
||||
name: 'property',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getContactCustomProperties',
|
||||
},
|
||||
default: '',
|
||||
description: 'Name of the property.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value of the property',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Date of Birth',
|
||||
name: 'dateOfBirth',
|
||||
|
|
|
@ -307,6 +307,25 @@ export class Hubspot implements INodeType {
|
|||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the contact properties to display them to user so that he can
|
||||
// select them easily
|
||||
async getContactCustomProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const endpoint = '/properties/v2/contacts/properties';
|
||||
const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {});
|
||||
for (const property of properties) {
|
||||
if (property.hubspotDefined === null) {
|
||||
const propertyName = property.label;
|
||||
const propertyId = property.name;
|
||||
returnData.push({
|
||||
name: propertyName,
|
||||
value: propertyId,
|
||||
});
|
||||
}
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the contact number of employees options to display them to user so that he can
|
||||
// select them easily
|
||||
async getContactNumberOfEmployees(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
|
@ -1064,6 +1083,20 @@ export class Hubspot implements INodeType {
|
|||
value: additionalFields.workEmail,
|
||||
});
|
||||
}
|
||||
|
||||
if (additionalFields.customPropertiesUi) {
|
||||
const customProperties = (additionalFields.customPropertiesUi as IDataObject).customPropertiesValues as IDataObject[];
|
||||
|
||||
if (customProperties) {
|
||||
for (const customProperty of customProperties) {
|
||||
body.push({
|
||||
property: customProperty.property,
|
||||
value: customProperty.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const endpoint = `/contacts/v1/contact/createOrUpdate/email/${email}`;
|
||||
responseData = await hubspotApiRequest.call(this, 'POST', endpoint, { properties: body });
|
||||
|
||||
|
|
|
@ -10,10 +10,10 @@ import {
|
|||
|
||||
export async function salesforceApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('salesforceOAuth2Api');
|
||||
const subdomain = (credentials!.accessTokenUrl as string).split('.')[0].split('/')[2];
|
||||
const subdomain = ((credentials!.accessTokenUrl as string).match(/https:\/\/(.+).salesforce\.com/) || [])[1]
|
||||
const options: OptionsWithUri = {
|
||||
method,
|
||||
body,
|
||||
body: method === "GET" ? undefined : body,
|
||||
qs,
|
||||
uri: uri || `https://${subdomain}.salesforce.com/services/data/v39.0${resource}`,
|
||||
json: true
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,66 +1,37 @@
|
|||
import { OptionsWithUri } from 'request';
|
||||
import {
|
||||
OptionsWithUri,
|
||||
} from 'request';
|
||||
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
IHookFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IExecuteSingleFunctions
|
||||
} from 'n8n-core';
|
||||
|
||||
import * as _ from 'lodash';
|
||||
|
||||
export const filterAndExecuteForEachTask = async function(
|
||||
this: IExecuteSingleFunctions,
|
||||
taskCallback: (t: any) => any
|
||||
) {
|
||||
const expression = this.getNodeParameter('expression') as string;
|
||||
const projectId = this.getNodeParameter('project') as number;
|
||||
// Enable regular expressions
|
||||
const reg = new RegExp(expression);
|
||||
const tasks = await todoistApiRequest.call(this, '/tasks', 'GET');
|
||||
const filteredTasks = tasks.filter(
|
||||
// Make sure that project will match no matter what the type is. If project was not selected match all projects
|
||||
(el: any) => (!projectId || el.project_id) && el.content.match(reg)
|
||||
);
|
||||
return {
|
||||
affectedTasks: (
|
||||
await Promise.all(filteredTasks.map((t: any) => taskCallback(t)))
|
||||
)
|
||||
// This makes it more clear and informative. We pass the ID as a convention and content to give the user confirmation that his/her expression works as expected
|
||||
.map(
|
||||
(el, i) =>
|
||||
el || { id: filteredTasks[i].id, content: filteredTasks[i].content }
|
||||
)
|
||||
};
|
||||
};
|
||||
import {
|
||||
IDataObject,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export async function todoistApiRequest(
|
||||
this:
|
||||
| IHookFunctions
|
||||
| IExecuteFunctions
|
||||
| IExecuteSingleFunctions
|
||||
| ILoadOptionsFunctions,
|
||||
resource: string,
|
||||
method: string,
|
||||
body: any = {},
|
||||
headers?: object
|
||||
): Promise<any> {
|
||||
// tslint:disable-line:no-any
|
||||
const credentials = this.getCredentials('todoistApi');
|
||||
|
||||
if (credentials === undefined) {
|
||||
throw new Error('No credentials got returned!');
|
||||
}
|
||||
|
||||
const headerWithAuthentication = Object.assign({}, headers, { Authorization: `Bearer ${credentials.apiKey}` });
|
||||
resource: string,
|
||||
body: any = {}, // tslint:disable-line:no-any
|
||||
qs: IDataObject = {},
|
||||
): Promise<any> { // tslint:disable-line:no-any
|
||||
const authentication = this.getNodeParameter('authentication', 0, 'apiKey');
|
||||
|
||||
const endpoint = 'api.todoist.com/rest/v1';
|
||||
|
||||
const options: OptionsWithUri = {
|
||||
headers: headerWithAuthentication,
|
||||
headers: {},
|
||||
method,
|
||||
qs,
|
||||
uri: `https://${endpoint}${resource}`,
|
||||
json: true
|
||||
json: true,
|
||||
};
|
||||
|
||||
if (Object.keys(body).length !== 0) {
|
||||
|
@ -68,13 +39,25 @@ export async function todoistApiRequest(
|
|||
}
|
||||
|
||||
try {
|
||||
return this.helpers.request!(options);
|
||||
if (authentication === 'apiKey') {
|
||||
const credentials = this.getCredentials('todoistApi') as IDataObject;
|
||||
|
||||
//@ts-ignore
|
||||
options.headers['Authorization'] = `Bearer ${credentials.apiKey}`;
|
||||
|
||||
return this.helpers.request!(options);
|
||||
} else {
|
||||
//@ts-ignore
|
||||
return await this.helpers.requestOAuth2.call(this, 'todoistOAuth2Api', options);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const errorMessage = error.response.body.message || error.response.body.Message;
|
||||
const errorMessage = error.response.body;
|
||||
|
||||
if (errorMessage !== undefined) {
|
||||
throw errorMessage;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
throw error.response.body;
|
||||
|
||||
throw errorMessage;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
IExecuteSingleFunctions,
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
} from 'n8n-core';
|
||||
|
||||
import {
|
||||
IDataObject,
|
||||
INodeTypeDescription,
|
||||
|
@ -9,12 +10,11 @@ import {
|
|||
ILoadOptionsFunctions,
|
||||
INodePropertyOptions,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
todoistApiRequest,
|
||||
filterAndExecuteForEachTask,
|
||||
} from './GenericFunctions';
|
||||
|
||||
|
||||
interface IBodyCreateTask {
|
||||
content: string;
|
||||
project_id?: number;
|
||||
|
@ -48,9 +48,44 @@ export class Todoist implements INodeType {
|
|||
{
|
||||
name: 'todoistApi',
|
||||
required: true,
|
||||
}
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'apiKey',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'todoistOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: [
|
||||
'oAuth2',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'API Key',
|
||||
value: 'apiKey',
|
||||
},
|
||||
{
|
||||
name: 'OAuth2',
|
||||
value: 'oAuth2',
|
||||
},
|
||||
],
|
||||
default: 'apiKey',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
|
@ -85,24 +120,29 @@ export class Todoist implements INodeType {
|
|||
description: 'Create a new task',
|
||||
},
|
||||
{
|
||||
name: 'Close by ID',
|
||||
value: 'close_id',
|
||||
description: 'Close a task by passing an ID',
|
||||
name: 'Close',
|
||||
value: 'close',
|
||||
description: 'Close a task',
|
||||
},
|
||||
{
|
||||
name: 'Close matching',
|
||||
value: 'close_match',
|
||||
description: 'Close a task by passing a regular expression',
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a task',
|
||||
},
|
||||
{
|
||||
name: 'Delete by ID',
|
||||
value: 'delete_id',
|
||||
description: 'Delete a task by passing an ID',
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a task',
|
||||
},
|
||||
{
|
||||
name: 'Delete matching',
|
||||
value: 'delete_match',
|
||||
description: 'Delete a task by passing a regular expression',
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all tasks',
|
||||
},
|
||||
{
|
||||
name: 'Reopen',
|
||||
value: 'reopen',
|
||||
description: 'Reopen a task',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
|
@ -122,9 +162,7 @@ export class Todoist implements INodeType {
|
|||
],
|
||||
operation: [
|
||||
'create',
|
||||
'close_match',
|
||||
'delete_match',
|
||||
]
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
|
@ -144,7 +182,7 @@ export class Todoist implements INodeType {
|
|||
],
|
||||
operation: [
|
||||
'create',
|
||||
]
|
||||
],
|
||||
},
|
||||
},
|
||||
default: [],
|
||||
|
@ -165,7 +203,7 @@ export class Todoist implements INodeType {
|
|||
],
|
||||
operation: [
|
||||
'create',
|
||||
]
|
||||
],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
|
@ -173,32 +211,27 @@ export class Todoist implements INodeType {
|
|||
description: 'Task content',
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
displayName: 'Task ID',
|
||||
name: 'taskId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
typeOptions: { rows: 1 },
|
||||
displayOptions: {
|
||||
show: { resource: ['task'], operation: ['close_id', 'delete_id'] }
|
||||
show: {
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
operation: [
|
||||
'delete',
|
||||
'close',
|
||||
'get',
|
||||
'reopen',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Expression to match',
|
||||
name: 'expression',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
typeOptions: { rows: 1 },
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['task'],
|
||||
operation: ['close_match', 'delete_match']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
displayName: 'Additional Fields',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
|
@ -210,22 +243,10 @@ export class Todoist implements INodeType {
|
|||
],
|
||||
operation: [
|
||||
'create',
|
||||
]
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Priority',
|
||||
name: 'priority',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
numberStepSize: 1,
|
||||
maxValue: 4,
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
description: 'Task priority from 1 (normal) to 4 (urgent).',
|
||||
},
|
||||
{
|
||||
displayName: 'Due Date Time',
|
||||
name: 'dueDateTime',
|
||||
|
@ -240,24 +261,131 @@ export class Todoist implements INodeType {
|
|||
default: '',
|
||||
description: 'Human defined task due date (ex.: “next Monday”, “Tomorrow”). Value is set using local (not UTC) time.',
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
{
|
||||
displayName: 'Priority',
|
||||
name: 'priority',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
numberStepSize: 1,
|
||||
maxValue: 4,
|
||||
minValue: 1,
|
||||
},
|
||||
default: 1,
|
||||
description: 'Task priority from 1 (normal) to 4 (urgent).',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'If all results should be returned or only up to a given limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 500,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'task',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Filter',
|
||||
name: 'filter',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Filter by any <a href="https://get.todoist.help/hc/en-us/articles/205248842">supported filter.</a>',
|
||||
},
|
||||
{
|
||||
displayName: 'IDs',
|
||||
name: 'ids',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A list of the task IDs to retrieve, this should be a comma separated list.',
|
||||
},
|
||||
{
|
||||
displayName: 'Label ID',
|
||||
name: 'labelId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLabels',
|
||||
},
|
||||
default: {},
|
||||
description: 'Filter tasks by label.',
|
||||
},
|
||||
{
|
||||
displayName: 'Lang',
|
||||
name: 'lang',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'IETF language tag defining what language filter is written in, if differs from default English',
|
||||
},
|
||||
{
|
||||
displayName: 'Project ID',
|
||||
name: 'projectId',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getProjects',
|
||||
},
|
||||
default: '',
|
||||
description: 'Filter tasks by project id.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
methods = {
|
||||
loadOptions: {
|
||||
// Get all the available projects to display them to user so that he can
|
||||
// select them easily
|
||||
async getProjects(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
let projects;
|
||||
try {
|
||||
projects = await todoistApiRequest.call(this, '/projects', 'GET');
|
||||
} catch (err) {
|
||||
throw new Error(`Todoist Error: ${err}`);
|
||||
}
|
||||
const projects = await todoistApiRequest.call(this, 'GET', '/projects');
|
||||
for (const project of projects) {
|
||||
const projectName = project.name;
|
||||
const projectId = project.id;
|
||||
|
@ -275,12 +403,8 @@ export class Todoist implements INodeType {
|
|||
// select them easily
|
||||
async getLabels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
let labels;
|
||||
try {
|
||||
labels = await todoistApiRequest.call(this, '/labels', 'GET');
|
||||
} catch (err) {
|
||||
throw new Error(`Todoist Error: ${err}`);
|
||||
}
|
||||
const labels = await todoistApiRequest.call(this, 'GET', '/labels');
|
||||
|
||||
for (const label of labels) {
|
||||
const labelName = label.name;
|
||||
const labelId = label.id;
|
||||
|
@ -296,67 +420,113 @@ export class Todoist implements INodeType {
|
|||
}
|
||||
};
|
||||
|
||||
async executeSingle(this: IExecuteSingleFunctions): Promise<INodeExecutionData> {
|
||||
const resource = this.getNodeParameter('resource') as string;
|
||||
const operation = this.getNodeParameter('operation') as string;
|
||||
try {
|
||||
return {
|
||||
json: { result: await OPERATIONS[resource]?.[operation]?.bind(this)() }
|
||||
};
|
||||
} catch (err) {
|
||||
return { json: { error: `Todoist Error: ${err.message}` } };
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: IDataObject[] = [];
|
||||
const length = items.length as unknown as number;
|
||||
const qs: IDataObject = {};
|
||||
let responseData;
|
||||
|
||||
const resource = this.getNodeParameter('resource', 0) as string;
|
||||
const operation = this.getNodeParameter('operation', 0) as string;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
|
||||
if (resource === 'task') {
|
||||
if (operation === 'create') {
|
||||
//https://developer.todoist.com/rest/v1/#create-a-new-task
|
||||
const content = this.getNodeParameter('content', i) as string;
|
||||
const projectId = this.getNodeParameter('project', i) as number;
|
||||
const labels = this.getNodeParameter('labels', i) as number[];
|
||||
const options = this.getNodeParameter('options', i) as IDataObject;
|
||||
|
||||
const body: IBodyCreateTask = {
|
||||
content,
|
||||
project_id: projectId,
|
||||
priority: (options.priority!) ? parseInt(options.priority as string, 10) : 1,
|
||||
};
|
||||
|
||||
if (options.dueDateTime) {
|
||||
body.due_datetime = options.dueDateTime as string;
|
||||
}
|
||||
|
||||
if (options.dueString) {
|
||||
body.due_string = options.dueString as string;
|
||||
}
|
||||
|
||||
if (labels !== undefined && labels.length !== 0) {
|
||||
body.label_ids = labels;
|
||||
}
|
||||
|
||||
responseData = await todoistApiRequest.call(this, 'POST', '/tasks', body);
|
||||
}
|
||||
if (operation === 'close') {
|
||||
//https://developer.todoist.com/rest/v1/#close-a-task
|
||||
const id = this.getNodeParameter('taskId', i) as string;
|
||||
|
||||
responseData = await todoistApiRequest.call(this, 'POST', `/tasks/${id}/close`);
|
||||
|
||||
responseData = { success: true };
|
||||
|
||||
}
|
||||
if (operation === 'delete') {
|
||||
//https://developer.todoist.com/rest/v1/#delete-a-task
|
||||
const id = this.getNodeParameter('taskId', i) as string;
|
||||
|
||||
responseData = await todoistApiRequest.call(this, 'DELETE', `/tasks/${id}`);
|
||||
|
||||
responseData = { success: true };
|
||||
|
||||
}
|
||||
if (operation === 'get') {
|
||||
//https://developer.todoist.com/rest/v1/#get-an-active-task
|
||||
const id = this.getNodeParameter('taskId', i) as string;
|
||||
|
||||
responseData = await todoistApiRequest.call(this, 'GET', `/tasks/${id}`);
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
//https://developer.todoist.com/rest/v1/#get-active-tasks
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
const filters = this.getNodeParameter('filters', i) as IDataObject;
|
||||
if (filters.projectId) {
|
||||
qs.project_id = filters.projectId as string;
|
||||
}
|
||||
if (filters.labelId) {
|
||||
qs.label_id = filters.labelId as string;
|
||||
}
|
||||
if (filters.filter) {
|
||||
qs.filter = filters.filter as string;
|
||||
}
|
||||
if (filters.lang) {
|
||||
qs.lang = filters.lang as string;
|
||||
}
|
||||
if (filters.ids) {
|
||||
qs.ids = filters.ids as string;
|
||||
}
|
||||
|
||||
responseData = await todoistApiRequest.call(this, 'GET', '/tasks', {}, qs);
|
||||
|
||||
if (!returnAll) {
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
responseData = responseData.splice(0, limit);
|
||||
}
|
||||
}
|
||||
if (operation === 'reopen') {
|
||||
//https://developer.todoist.com/rest/v1/#get-an-active-task
|
||||
const id = this.getNodeParameter('taskId', i) as string;
|
||||
|
||||
responseData = await todoistApiRequest.call(this, 'POST', `/tasks/${id}/reopen`);
|
||||
|
||||
responseData = { success: true };
|
||||
}
|
||||
}
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else {
|
||||
returnData.push(responseData as IDataObject);
|
||||
}
|
||||
}
|
||||
|
||||
return [this.helpers.returnJsonArray(returnData)];
|
||||
}
|
||||
}
|
||||
|
||||
const OPERATIONS: {
|
||||
[key: string]: { [key: string]: (this: IExecuteSingleFunctions) => any };
|
||||
} = {
|
||||
task: {
|
||||
create(this: IExecuteSingleFunctions) {
|
||||
//https://developer.todoist.com/rest/v1/#create-a-new-task
|
||||
const content = this.getNodeParameter('content') as string;
|
||||
const projectId = this.getNodeParameter('project') as number;
|
||||
const labels = this.getNodeParameter('labels') as number[];
|
||||
const options = this.getNodeParameter('options') as IDataObject;
|
||||
|
||||
const body: IBodyCreateTask = {
|
||||
content,
|
||||
project_id: projectId,
|
||||
priority: (options.priority!) ? parseInt(options.priority as string, 10) : 1,
|
||||
};
|
||||
|
||||
if (options.dueDateTime) {
|
||||
body.due_datetime = options.dueDateTime as string;
|
||||
}
|
||||
|
||||
if (options.dueString) {
|
||||
body.due_string = options.dueString as string;
|
||||
}
|
||||
|
||||
if (labels !== undefined && labels.length !== 0) {
|
||||
body.label_ids = labels;
|
||||
}
|
||||
|
||||
return todoistApiRequest.call(this, '/tasks', 'POST', body);
|
||||
},
|
||||
close_id(this: IExecuteSingleFunctions) {
|
||||
const id = this.getNodeParameter('id') as string;
|
||||
return todoistApiRequest.call(this, `/tasks/${id}/close`, 'POST');
|
||||
},
|
||||
delete_id(this: IExecuteSingleFunctions) {
|
||||
const id = this.getNodeParameter('id') as string;
|
||||
return todoistApiRequest.call(this, `/tasks/${id}`, 'DELETE');
|
||||
},
|
||||
close_match(this) {
|
||||
return filterAndExecuteForEachTask.call(this, t =>
|
||||
todoistApiRequest.call(this, `/tasks/${t.id}/close`, 'POST')
|
||||
);
|
||||
},
|
||||
delete_match(this) {
|
||||
return filterAndExecuteForEachTask.call(this, t =>
|
||||
todoistApiRequest.call(this, `/tasks/${t.id}`, 'DELETE')
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 5.5 KiB |
|
@ -23,7 +23,10 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions
|
|||
body,
|
||||
//@ts-ignore
|
||||
uri,
|
||||
json: true
|
||||
json: true,
|
||||
qsStringifyOptions: {
|
||||
arrayFormat: 'brackets',
|
||||
},
|
||||
};
|
||||
|
||||
options = Object.assign({}, options, option);
|
||||
|
@ -56,6 +59,7 @@ export async function zendeskApiRequest(this: IHookFunctions | IExecuteFunctions
|
|||
return await this.helpers.requestOAuth2!.call(this, 'zendeskOAuth2Api', options);
|
||||
}
|
||||
} catch(err) {
|
||||
|
||||
let errorMessage = err.message;
|
||||
if (err.response && err.response.body && err.response.body.error) {
|
||||
errorMessage = err.response.body.error;
|
||||
|
|
692
packages/nodes-base/nodes/Zendesk/UserDescription.ts
Normal file
692
packages/nodes-base/nodes/Zendesk/UserDescription.ts
Normal file
|
@ -0,0 +1,692 @@
|
|||
import {
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export const userOperations = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a user',
|
||||
},
|
||||
{
|
||||
name: 'Delete',
|
||||
value: 'delete',
|
||||
description: 'Delete a user',
|
||||
},
|
||||
{
|
||||
name: 'Get',
|
||||
value: 'get',
|
||||
description: 'Get a user',
|
||||
},
|
||||
{
|
||||
name: 'Get All',
|
||||
value: 'getAll',
|
||||
description: 'Get all users',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: 'update',
|
||||
description: 'Update a user',
|
||||
},
|
||||
],
|
||||
default: 'create',
|
||||
description: 'The operation to perform.',
|
||||
},
|
||||
] as INodeProperties[];
|
||||
|
||||
export const userFields = [
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* user:create */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
description: `The user's name`,
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Alias',
|
||||
name: 'alis',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `An alias displayed to end users`,
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Role ID',
|
||||
name: 'custom_role_id',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: `A custom role if the user is an agent on the Enterprise plan`,
|
||||
},
|
||||
{
|
||||
displayName: 'Details',
|
||||
name: 'details',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Any details you want to store about the user, such as an address',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The user's primary email address`,
|
||||
},
|
||||
{
|
||||
displayName: 'External ID',
|
||||
name: 'externalId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A unique identifier from another system',
|
||||
},
|
||||
{
|
||||
displayName: 'Locale ID',
|
||||
name: 'locale',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLocales',
|
||||
},
|
||||
default: '',
|
||||
description: `The user's locale.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Moderator',
|
||||
name: 'moderator',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Designates whether the user has forum moderation capabilities',
|
||||
},
|
||||
{
|
||||
displayName: 'Notes',
|
||||
name: 'notes',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Any notes you want to store about the user',
|
||||
},
|
||||
{
|
||||
displayName: 'Only Private Comments',
|
||||
name: 'only_private_comments',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `true if the user can only create private comments`,
|
||||
},
|
||||
{
|
||||
displayName: 'Organization ID',
|
||||
name: 'organizationId',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: `The id of the user's organization. If the user has more than one organization memberships, the id of the user's default organization`,
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The user's primary phone number.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Report CSV',
|
||||
name: 'report_csv',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `Whether or not the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Restricted Agent',
|
||||
name: 'restricted_agent',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `If the agent has any restrictions; false for admins and unrestricted agents, true for other agents`,
|
||||
},
|
||||
{
|
||||
displayName: 'Role',
|
||||
name: 'role',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'End User',
|
||||
value: 'end-user',
|
||||
},
|
||||
{
|
||||
name: 'Agent',
|
||||
value: 'agent',
|
||||
},
|
||||
{
|
||||
name: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: `The user's role`,
|
||||
},
|
||||
{
|
||||
displayName: 'Signature',
|
||||
name: 'signature',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The user's signature. Only agents and admins can have signatures`,
|
||||
},
|
||||
{
|
||||
displayName: 'Suspended',
|
||||
name: 'suspended',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `If the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal`,
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'multiOptions',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTags',
|
||||
},
|
||||
default: [],
|
||||
description: 'The array of tags applied to this user',
|
||||
},
|
||||
{
|
||||
displayName: 'Ticket Restriction',
|
||||
name: 'ticket_restriction',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Organization',
|
||||
value: 'organization',
|
||||
},
|
||||
{
|
||||
name: 'Groups',
|
||||
value: 'groups',
|
||||
},
|
||||
{
|
||||
name: 'Assigned',
|
||||
value: 'assigned',
|
||||
},
|
||||
{
|
||||
name: 'Requested',
|
||||
value: 'requested',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: `Specifies which tickets the user has access to`,
|
||||
},
|
||||
{
|
||||
displayName: 'Timezone',
|
||||
name: 'time_zone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The user's time zone.`,
|
||||
},
|
||||
{
|
||||
displayName: 'User Fields',
|
||||
name: 'userFieldsUi',
|
||||
placeholder: 'Add User Field',
|
||||
description: `Values of custom fields in the user's profile.`,
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'userFieldValues',
|
||||
displayName: 'Field',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field',
|
||||
name: 'field',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUserFields',
|
||||
},
|
||||
default: '',
|
||||
description: 'Name of the field to sort on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value of the field.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Verified',
|
||||
name: 'verified',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `The user's primary identity is verified or not`,
|
||||
},
|
||||
],
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* user:update */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'User ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
operation: [
|
||||
'update',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'User ID',
|
||||
},
|
||||
{
|
||||
displayName: 'Update Fields',
|
||||
name: 'updateFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
operation: [
|
||||
'update',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Alias',
|
||||
name: 'alis',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `An alias displayed to end users`,
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Role ID',
|
||||
name: 'custom_role_id',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: `A custom role if the user is an agent on the Enterprise plan`,
|
||||
},
|
||||
{
|
||||
displayName: 'Details',
|
||||
name: 'details',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Any details you want to store about the user, such as an address',
|
||||
},
|
||||
{
|
||||
displayName: 'Email',
|
||||
name: 'email',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The user's primary email address`,
|
||||
},
|
||||
{
|
||||
displayName: 'External ID',
|
||||
name: 'externalId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'A unique identifier from another system',
|
||||
},
|
||||
{
|
||||
displayName: 'Locale ID',
|
||||
name: 'locale',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getLocales',
|
||||
},
|
||||
default: '',
|
||||
description: `The user's locale.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Moderator',
|
||||
name: 'moderator',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Designates whether the user has forum moderation capabilities',
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'name',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The user's name`,
|
||||
},
|
||||
{
|
||||
displayName: 'Notes',
|
||||
name: 'notes',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Any notes you want to store about the user',
|
||||
},
|
||||
{
|
||||
displayName: 'Only Private Comments',
|
||||
name: 'only_private_comments',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `true if the user can only create private comments`,
|
||||
},
|
||||
{
|
||||
displayName: 'Organization ID',
|
||||
name: 'organizationId',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: `The id of the user's organization. If the user has more than one organization memberships, the id of the user's default organization`,
|
||||
},
|
||||
{
|
||||
displayName: 'Phone',
|
||||
name: 'phone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The user's primary phone number.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Report CSV',
|
||||
name: 'report_csv',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `Whether or not the user can access the CSV report on the Search tab of the Reporting page in the Support admin interface.`,
|
||||
},
|
||||
{
|
||||
displayName: 'Restricted Agent',
|
||||
name: 'restricted_agent',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `If the agent has any restrictions; false for admins and unrestricted agents, true for other agents`,
|
||||
},
|
||||
{
|
||||
displayName: 'Role',
|
||||
name: 'role',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'End User',
|
||||
value: 'end-user',
|
||||
},
|
||||
{
|
||||
name: 'Agent',
|
||||
value: 'agent',
|
||||
},
|
||||
{
|
||||
name: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: `The user's role`,
|
||||
},
|
||||
{
|
||||
displayName: 'Signature',
|
||||
name: 'signature',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The user's signature. Only agents and admins can have signatures`,
|
||||
},
|
||||
{
|
||||
displayName: 'Suspended',
|
||||
name: 'suspended',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `If the agent is suspended. Tickets from suspended users are also suspended, and these users cannot sign in to the end user portal`,
|
||||
},
|
||||
{
|
||||
displayName: 'Tags',
|
||||
name: 'tags',
|
||||
type: 'multiOptions',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getTags',
|
||||
},
|
||||
default: [],
|
||||
description: 'The array of tags applied to this user',
|
||||
},
|
||||
{
|
||||
displayName: 'Ticket Restriction',
|
||||
name: 'ticket_restriction',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Organization',
|
||||
value: 'organization',
|
||||
},
|
||||
{
|
||||
name: 'Groups',
|
||||
value: 'groups',
|
||||
},
|
||||
{
|
||||
name: 'Assigned',
|
||||
value: 'assigned',
|
||||
},
|
||||
{
|
||||
name: 'Requested',
|
||||
value: 'requested',
|
||||
},
|
||||
],
|
||||
default: '',
|
||||
description: `Specifies which tickets the user has access to`,
|
||||
},
|
||||
{
|
||||
displayName: 'Timezone',
|
||||
name: 'time_zone',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: `The user's time zone.`,
|
||||
},
|
||||
{
|
||||
displayName: 'User Fields',
|
||||
name: 'userFieldsUi',
|
||||
placeholder: 'Add User Field',
|
||||
description: `Values of custom fields in the user's profile.`,
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'userFieldValues',
|
||||
displayName: 'Field',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Field',
|
||||
name: 'field',
|
||||
type: 'options',
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getUserFields',
|
||||
},
|
||||
default: '',
|
||||
description: 'Name of the field to sort on.',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'value',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Value of the field.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Verified',
|
||||
name: 'verified',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: `The user's primary identity is verified or not`,
|
||||
},
|
||||
],
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* user:get */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'User ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
operation: [
|
||||
'get',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'User ID',
|
||||
},
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* user:getAll */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: false,
|
||||
description: 'If all results should be returned or only up to a given limit.',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
returnAll: [
|
||||
false,
|
||||
],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 100,
|
||||
},
|
||||
default: 100,
|
||||
description: 'How many results to return.',
|
||||
},
|
||||
{
|
||||
displayName: 'Filters',
|
||||
name: 'filters',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Filter',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
operation: [
|
||||
'getAll',
|
||||
],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Roles',
|
||||
name: 'role',
|
||||
type: 'multiOptions',
|
||||
options: [
|
||||
{
|
||||
name: 'End User',
|
||||
value: 'end-user',
|
||||
},
|
||||
{
|
||||
name: 'Agent',
|
||||
value: 'agent',
|
||||
},
|
||||
{
|
||||
name: 'Admin',
|
||||
value: 'admin',
|
||||
},
|
||||
],
|
||||
default: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* user:delete */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
{
|
||||
displayName: 'User ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: [
|
||||
'user',
|
||||
],
|
||||
operation: [
|
||||
'delete',
|
||||
],
|
||||
},
|
||||
},
|
||||
description: 'User ID',
|
||||
},
|
||||
] as INodeProperties[];
|
|
@ -27,11 +27,15 @@ import {
|
|||
ticketFieldOperations
|
||||
} from './TicketFieldDescription';
|
||||
|
||||
import {
|
||||
userFields,
|
||||
userOperations
|
||||
} from './UserDescription';
|
||||
|
||||
import {
|
||||
ITicket,
|
||||
IComment,
|
||||
} from './TicketInterface';
|
||||
import { response } from 'express';
|
||||
|
||||
export class Zendesk implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
|
@ -105,6 +109,11 @@ export class Zendesk implements INodeType {
|
|||
value: 'ticketField',
|
||||
description: 'Manage system and custom ticket fields',
|
||||
},
|
||||
{
|
||||
name: 'User',
|
||||
value: 'user',
|
||||
description: 'Manage users',
|
||||
},
|
||||
],
|
||||
default: 'ticket',
|
||||
description: 'Resource to consume.',
|
||||
|
@ -112,9 +121,12 @@ export class Zendesk implements INodeType {
|
|||
// TICKET
|
||||
...ticketOperations,
|
||||
...ticketFields,
|
||||
// TICKET FIELDS
|
||||
// TICKET FIELD
|
||||
...ticketFieldOperations,
|
||||
...ticketFieldFields,
|
||||
// USER
|
||||
...userOperations,
|
||||
...userFields,
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -177,6 +189,38 @@ export class Zendesk implements INodeType {
|
|||
}
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the locales to display them to user so that he can
|
||||
// select them easily
|
||||
async getLocales(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const locales = await zendeskApiRequestAllItems.call(this, 'locales', 'GET', '/locales');
|
||||
for (const locale of locales) {
|
||||
const localeName = `${locale.locale} - ${locale.name}`;
|
||||
const localeId = locale.locale;
|
||||
returnData.push({
|
||||
name: localeName,
|
||||
value: localeId,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
|
||||
// Get all the user fields to display them to user so that he can
|
||||
// select them easily
|
||||
async getUserFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const fields = await zendeskApiRequestAllItems.call(this, 'user_fields', 'GET', '/user_fields');
|
||||
for (const field of fields) {
|
||||
const fieldName = field.title;
|
||||
const fieldId = field.key;
|
||||
returnData.push({
|
||||
name: fieldName,
|
||||
value: fieldId,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -359,6 +403,87 @@ export class Zendesk implements INodeType {
|
|||
}
|
||||
}
|
||||
}
|
||||
//https://developer.zendesk.com/rest_api/docs/support/users
|
||||
if (resource === 'user') {
|
||||
//https://developer.zendesk.com/rest_api/docs/support/users#create-user
|
||||
if (operation === 'create') {
|
||||
const name = this.getNodeParameter('name', i) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject;
|
||||
|
||||
const body: IDataObject = {
|
||||
name,
|
||||
};
|
||||
|
||||
Object.assign(body, additionalFields);
|
||||
|
||||
if (body.userFieldsUi) {
|
||||
const userFields = (body.userFieldsUi as IDataObject).userFieldValues as IDataObject[];
|
||||
if (userFields) {
|
||||
body.user_fields = {};
|
||||
for (const userField of userFields) {
|
||||
//@ts-ignore
|
||||
body.user_fields[userField.field] = userField.value;
|
||||
}
|
||||
delete body.userFieldsUi;
|
||||
}
|
||||
}
|
||||
|
||||
responseData = await zendeskApiRequest.call(this, 'POST', '/users', { user: body });
|
||||
responseData = responseData.user;
|
||||
}
|
||||
//https://developer.zendesk.com/rest_api/docs/support/tickets#update-ticket
|
||||
if (operation === 'update') {
|
||||
const userId = this.getNodeParameter('id', i) as string;
|
||||
const updateFields = this.getNodeParameter('updateFields', i) as IDataObject;
|
||||
|
||||
const body: IDataObject = {};
|
||||
|
||||
Object.assign(body, updateFields);
|
||||
|
||||
if (body.userFieldsUi) {
|
||||
const userFields = (body.userFieldsUi as IDataObject).userFieldValues as IDataObject[];
|
||||
if (userFields) {
|
||||
body.user_fields = {};
|
||||
for (const userField of userFields) {
|
||||
//@ts-ignore
|
||||
body.user_fields[userField.field] = userField.value;
|
||||
}
|
||||
delete body.userFieldsUi;
|
||||
}
|
||||
}
|
||||
|
||||
responseData = await zendeskApiRequest.call(this, 'PUT', `/users/${userId}`, { user: body });
|
||||
responseData = responseData.user;
|
||||
}
|
||||
//https://developer.zendesk.com/rest_api/docs/support/users#show-user
|
||||
if (operation === 'get') {
|
||||
const userId = this.getNodeParameter('id', i) as string;
|
||||
responseData = await zendeskApiRequest.call(this, 'GET', `/users/${userId}`, {});
|
||||
responseData = responseData.user;
|
||||
}
|
||||
//https://developer.zendesk.com/rest_api/docs/support/users#list-users
|
||||
if (operation === 'getAll') {
|
||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||
const options = this.getNodeParameter('filters', i) as IDataObject;
|
||||
|
||||
Object.assign(qs, options);
|
||||
|
||||
if (returnAll) {
|
||||
responseData = await zendeskApiRequestAllItems.call(this, 'users', 'GET', `/users`, {}, qs);
|
||||
} else {
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
qs.per_page = limit;
|
||||
responseData = await zendeskApiRequest.call(this, 'GET', `/users`, {}, qs);
|
||||
responseData = responseData.users;
|
||||
}
|
||||
}
|
||||
//https://developer.zendesk.com/rest_api/docs/support/users#delete-user
|
||||
if (operation === 'delete') {
|
||||
const userId = this.getNodeParameter('id', i) as string;
|
||||
responseData = await zendeskApiRequest.call(this, 'DELETE', `/users/${userId}`, {});
|
||||
responseData = responseData.user;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(responseData)) {
|
||||
returnData.push.apply(returnData, responseData as IDataObject[]);
|
||||
} else {
|
||||
|
|
|
@ -375,14 +375,14 @@ export class ZendeskTrigger implements INodeType {
|
|||
displayName: 'All',
|
||||
values: [
|
||||
...conditionFields,
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'any',
|
||||
displayName: 'Any',
|
||||
values: [
|
||||
...conditionFields,
|
||||
]
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -435,17 +435,74 @@ export class ZendeskTrigger implements INodeType {
|
|||
webhookMethods = {
|
||||
default: {
|
||||
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default') as string;
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
if (webhookData.webhookId === undefined) {
|
||||
const conditions = this.getNodeParameter('conditions') as IDataObject;
|
||||
const conditionsAll = conditions.all as [IDataObject];
|
||||
|
||||
let endpoint = '';
|
||||
const aux: IDataObject = {};
|
||||
const resultAll = [], resultAny = [];
|
||||
|
||||
if (conditionsAll) {
|
||||
for (const conditionAll of conditionsAll) {
|
||||
aux.field = conditionAll.field;
|
||||
aux.operator = conditionAll.operation;
|
||||
if (conditionAll.operation !== 'changed'
|
||||
&& conditionAll.operation !== 'not_changed') {
|
||||
aux.value = conditionAll.value;
|
||||
} else {
|
||||
aux.value = null;
|
||||
}
|
||||
resultAll.push(aux);
|
||||
}
|
||||
}
|
||||
|
||||
const conditionsAny = conditions.any as [IDataObject];
|
||||
if (conditionsAny) {
|
||||
for (const conditionAny of conditionsAny) {
|
||||
aux.field = conditionAny.field;
|
||||
aux.operator = conditionAny.operation;
|
||||
if (conditionAny.operation !== 'changed'
|
||||
&& conditionAny.operation !== 'not_changed') {
|
||||
aux.value = conditionAny.value;
|
||||
} else {
|
||||
aux.value = null;
|
||||
}
|
||||
resultAny.push(aux);
|
||||
}
|
||||
}
|
||||
|
||||
// check if there is a target already created
|
||||
endpoint = `/targets`;
|
||||
const targets = await zendeskApiRequestAllItems.call(this, 'targets', 'GET', endpoint);
|
||||
for (const target of targets) {
|
||||
if (target.target_url === webhookUrl) {
|
||||
webhookData.targetId = target.id.toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// no target was found
|
||||
if (webhookData.targetId === undefined) {
|
||||
return false;
|
||||
}
|
||||
const endpoint = `/triggers/${webhookData.webhookId}`;
|
||||
try {
|
||||
await zendeskApiRequest.call(this, 'GET', endpoint);
|
||||
} catch (e) {
|
||||
return false;
|
||||
|
||||
endpoint = `/triggers/active`;
|
||||
const triggers = await zendeskApiRequestAllItems.call(this, 'triggers', 'GET', endpoint);
|
||||
for (const trigger of triggers) {
|
||||
const toDeleteTriggers = [];
|
||||
// this trigger belong to the current target
|
||||
if (trigger.actions[0].value[0].toString() === webhookData.targetId?.toString()) {
|
||||
toDeleteTriggers.push(trigger.id);
|
||||
}
|
||||
// delete all trigger attach to this target;
|
||||
if (toDeleteTriggers.length !== 0) {
|
||||
await zendeskApiRequest.call(this, 'DELETE', '/triggers/destroy_many', {}, { ids: toDeleteTriggers.join(',') } );
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
async create(this: IHookFunctions): Promise<boolean> {
|
||||
const webhookUrl = this.getNodeWebhookUrl('default') as string;
|
||||
|
@ -509,8 +566,8 @@ export class ZendeskTrigger implements INodeType {
|
|||
{
|
||||
field: 'notification_target',
|
||||
value: [],
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const bodyTarget: IDataObject = {
|
||||
|
@ -523,9 +580,21 @@ export class ZendeskTrigger implements INodeType {
|
|||
content_type: 'application/json',
|
||||
},
|
||||
};
|
||||
const { target } = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget);
|
||||
let target: IDataObject = {};
|
||||
|
||||
// if target id exists but trigger does not then reuse the target
|
||||
// and create the trigger else create both
|
||||
if (webhookData.targetId !== undefined) {
|
||||
target.id = webhookData.targetId;
|
||||
} else {
|
||||
target = await zendeskApiRequest.call(this, 'POST', '/targets', bodyTarget);
|
||||
target = target.target as IDataObject;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
bodyTrigger.trigger.actions[0].value = [target.id, JSON.stringify(message)];
|
||||
|
||||
//@ts-ignore
|
||||
const { trigger } = await zendeskApiRequest.call(this, 'POST', '/triggers', bodyTrigger);
|
||||
webhookData.webhookId = trigger.id;
|
||||
webhookData.targetId = target.id;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-nodes-base",
|
||||
"version": "0.73.0",
|
||||
"version": "0.74.0",
|
||||
"description": "Base nodes of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
|
@ -48,6 +48,7 @@
|
|||
"dist/credentials/CockpitApi.credentials.js",
|
||||
"dist/credentials/CodaApi.credentials.js",
|
||||
"dist/credentials/ContentfulApi.credentials.js",
|
||||
"dist/credentials/ConvertKitApi.credentials.js",
|
||||
"dist/credentials/CopperApi.credentials.js",
|
||||
"dist/credentials/CalendlyApi.credentials.js",
|
||||
"dist/credentials/CustomerIoApi.credentials.js",
|
||||
|
@ -150,6 +151,7 @@
|
|||
"dist/credentials/SurveyMonkeyOAuth2Api.credentials.js",
|
||||
"dist/credentials/TelegramApi.credentials.js",
|
||||
"dist/credentials/TodoistApi.credentials.js",
|
||||
"dist/credentials/TodoistOAuth2Api.credentials.js",
|
||||
"dist/credentials/TravisCiApi.credentials.js",
|
||||
"dist/credentials/TrelloApi.credentials.js",
|
||||
"dist/credentials/TwilioApi.credentials.js",
|
||||
|
@ -207,6 +209,8 @@
|
|||
"dist/nodes/Cockpit/Cockpit.node.js",
|
||||
"dist/nodes/Coda/Coda.node.js",
|
||||
"dist/nodes/Contentful/Contentful.node.js",
|
||||
"dist/nodes/ConvertKit/ConvertKit.node.js",
|
||||
"dist/nodes/ConvertKit/ConvertKitTrigger.node.js",
|
||||
"dist/nodes/Copper/CopperTrigger.node.js",
|
||||
"dist/nodes/CrateDb/CrateDb.node.js",
|
||||
"dist/nodes/Cron.node.js",
|
||||
|
|
Loading…
Reference in a new issue