Merge branch 'n8n-io:master' into Add-schema-registry-into-kafka

This commit is contained in:
Ricardo Georgel 2021-06-27 12:02:30 -03:00 committed by GitHub
commit 8a22cc7114
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
537 changed files with 11390 additions and 2115 deletions

View file

@ -14,3 +14,7 @@ indent_size = 2
[*.ts]
quote_type = single
[*.yml]
indent_style = space
indent_size = 2

View file

@ -1,40 +0,0 @@
name: Docker Image CI - Rpi
on:
push:
tags:
- n8n@*
workflow_dispatch:
inputs:
version:
description: 'n8n version to build docker image for.'
required: true
default: '0.112.0'
jobs:
armv7_job:
runs-on: ubuntu-18.04
name: Build on ARMv7 (Rpi)
steps:
- uses: actions/checkout@v1
- name: Get the version
id: vars
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:14})
- name: Log in to Docker registry
run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker Buildx
uses: crazy-max/ghaction-docker-buildx@v3
with:
buildx-version: latest
qemu-version: latest
- name: Run Buildx (push image)
if: success()
run: |
docker buildx build \
--platform linux/arm/v7 \
--build-arg N8N_VERSION=${{github.event.inputs.version || steps.vars.outputs.tag}} \
-t ${{ secrets.DOCKER_USERNAME }}/n8n:${{github.event.inputs.version || steps.vars.outputs.tag}}-rpi \
-t ${{ secrets.DOCKER_USERNAME }}/n8n:latest-rpi \
--output type=image,push=true docker/images/n8n-rpi

View file

@ -6,33 +6,41 @@ on:
- n8n@*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Get the version
id: vars
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:14})
- name: Log in to Docker registry
run: docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
- name: Build the Docker image of version
run: docker build --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}} docker/images/n8n
- name: Push Docker image of version
run: docker push n8nio/n8n:${{steps.vars.outputs.tag}}
- name: Tag Docker image with latest
run: docker tag n8nio/n8n:${{steps.vars.outputs.tag}} n8nio/n8n:latest
- name: Push docker images of latest
run: docker push n8nio/n8n:latest
- name: Build the Docker image of version (Debian)
run: docker build --build-arg N8N_VERSION=${{steps.vars.outputs.tag}} -t n8nio/n8n:${{steps.vars.outputs.tag}}-debian docker/images/n8n-debian
- name: Push Docker image of version (Debian)
run: docker push n8nio/n8n:${{steps.vars.outputs.tag}}-debian
- name: Tag Docker image with latest (Debian)
run: docker tag n8nio/n8n:${{steps.vars.outputs.tag}}-debian n8nio/n8n:latest-debian
- name: Push docker images of latest (Debian)
run: docker push n8nio/n8n:latest-debian
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build
uses: docker/build-push-action@v2
with:
context: ./docker/images/n8n
build-args: |
N8N_VERSION=${{steps.vars.outputs.tag}}
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/n8n:${{ steps.vars.outputs.tag }}
${{ secrets.DOCKER_USERNAME }}/n8n:latest
- name: Build (debian)
uses: docker/build-push-action@v2
with:
context: ./docker/images/n8n-debian
build-args: |
N8N_VERSION=${{ steps.vars.outputs.tag }}
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/n8n:${{ steps.vars.outputs.tag }}-debian
${{ secrets.DOCKER_USERNAME }}/n8n:latest-debian

View file

@ -29,7 +29,7 @@ FROM node:14.15-alpine
USER root
RUN apk add --update graphicsmagick tzdata tini su-exec
RUN apk add --update graphicsmagick tzdata tini su-exec git
WORKDIR /data

View file

@ -6,7 +6,7 @@ RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!"
RUN \
apt-get update && \
apt-get -y install graphicsmagick gosu
apt-get -y install graphicsmagick gosu git
# Set a custom user to not have n8n run as root
USER root

View file

@ -1,21 +0,0 @@
FROM arm32v7/node:14.15
ARG N8N_VERSION
RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi
RUN \
apt-get update && \
apt-get -y install graphicsmagick gosu
RUN npm_config_user=root npm install -g full-icu n8n@${N8N_VERSION}
ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu
ENV NODE_ENV production
WORKDIR /data
USER root
CMD chown -R node:node /home/node/.n8n \
&& gosu node n8n

View file

@ -1,22 +0,0 @@
## n8n - Raspberry PI Docker Image
Dockerfile to build n8n for Raspberry PI.
For information about how to run n8n with Docker check the generic
[Docker-Readme](https://github.com/n8n-io/n8n/tree/master/docker/images/n8n/README.md)
```
docker build --build-arg N8N_VERSION=<VERSION> -t n8nio/n8n:<VERSION> .
# For example:
docker build --build-arg N8N_VERSION=0.43.0 -t n8nio/n8n:0.43.0-rpi .
```
```
docker run -it --rm \
--name n8n \
-p 5678:5678 \
-v ~/.n8n:/home/node/.n8n \
n8nio/n8n:0.70.0-rpi
```

View file

@ -14,14 +14,16 @@ USER root
# it needs to build it correctly.
RUN apk --update add --virtual build-dependencies python build-base ca-certificates && \
npm_config_user=root npm install -g full-icu n8n@${N8N_VERSION} && \
apk del build-dependencies
apk del build-dependencies \
&& rm -rf /root /tmp/* /var/cache/apk/* && mkdir /root;
# Install fonts
RUN apk --no-cache add --virtual fonts msttcorefonts-installer fontconfig && \
update-ms-fonts && \
fc-cache -f && \
apk del fonts && \
find /usr/share/fonts/truetype/msttcorefonts/ -type l -exec unlink {} \;
find /usr/share/fonts/truetype/msttcorefonts/ -type l -exec unlink {} \; \
&& rm -rf /root /tmp/* /var/cache/apk/* && mkdir /root
ENV NODE_ICU_DATA /usr/local/lib/node_modules/full-icu

View file

@ -227,10 +227,10 @@ docker run -it --rm \
## Build Docker-Image
```
docker build --build-arg N8N_VERSION=<VERSION> -t n8nio/n8n:<VERSION> .
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --build-arg N8N_VERSION=<VERSION> -t n8nio/n8n:<VERSION> .
# For example:
docker build --build-arg N8N_VERSION=0.18.1 -t n8nio/n8n:0.18.1 .
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --build-arg N8N_VERSION=0.114.0 -t n8nio/n8n:0.114.0 .
```

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.122.3",
"version": "0.126.0",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -82,7 +82,7 @@
"dependencies": {
"@oclif/command": "^1.5.18",
"@oclif/errors": "^1.2.2",
"@types/jsonwebtoken": "^8.3.4",
"@types/jsonwebtoken": "^8.5.2",
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.3",
@ -106,10 +106,10 @@
"localtunnel": "^2.0.0",
"lodash.get": "^4.4.2",
"mysql2": "~2.2.0",
"n8n-core": "~0.73.0",
"n8n-editor-ui": "~0.92.2",
"n8n-nodes-base": "~0.119.2",
"n8n-workflow": "~0.60.0",
"n8n-core": "~0.75.0",
"n8n-editor-ui": "~0.96.0",
"n8n-nodes-base": "~0.123.0",
"n8n-workflow": "~0.62.0",
"oauth-1.0a": "^2.2.6",
"open": "^7.0.0",
"pg": "^8.3.0",

View file

@ -313,7 +313,6 @@ export class ActiveWorkflowRunner {
try {
await Db.collections.Webhook?.insert(webhook);
const webhookExists = await workflow.runWebhookMethod('checkExists', webhookData, NodeExecuteFunctions, mode, activation, false);
if (webhookExists !== true) {
// If webhook does not exist yet create it
@ -341,7 +340,7 @@ export class ActiveWorkflowRunner {
errorMessage = error.message;
}
throw new Error(errorMessage);
throw error;
}
}
// Save static data!

View file

@ -3,6 +3,7 @@ import {
UserSettings,
} from 'n8n-core';
import {
CodexData,
ICredentialType,
ILogger,
INodeType,
@ -25,6 +26,8 @@ import {
import * as glob from 'glob-promise';
import * as path from 'path';
const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
class LoadNodesAndCredentialsClass {
nodeTypes: INodeTypeData = {};
@ -133,7 +136,6 @@ class LoadNodesAndCredentialsClass {
* @param {string} credentialName The name of the credentials
* @param {string} filePath The file to read credentials from
* @returns {Promise<void>}
* @memberof N8nPackagesInformationClass
*/
async loadCredentialsFromFile(credentialName: string, filePath: string): Promise<void> {
const tempModule = require(filePath);
@ -160,7 +162,6 @@ class LoadNodesAndCredentialsClass {
* @param {string} nodeName Tha name of the node
* @param {string} filePath The file to read node from
* @returns {Promise<void>}
* @memberof N8nPackagesInformationClass
*/
async loadNodeFromFile(packageName: string, nodeName: string, filePath: string): Promise<void> {
let tempNode: INodeType;
@ -169,6 +170,7 @@ class LoadNodesAndCredentialsClass {
const tempModule = require(filePath);
try {
tempNode = new tempModule[nodeName]() as INodeType;
this.addCodex({ node: tempNode, filePath, isCustom: packageName === 'CUSTOM' });
} catch (error) {
console.error(`Error loading node "${nodeName}" from: "${filePath}"`);
throw error;
@ -202,6 +204,57 @@ class LoadNodesAndCredentialsClass {
};
}
/**
* Retrieves `categories`, `subcategories` and alias (if defined)
* from the codex data for the node at the given file path.
*
* @param {string} filePath The file path to a `*.node.js` file
* @returns {CodexData}
*/
getCodex(filePath: string): CodexData {
const { categories, subcategories, alias } = require(`${filePath}on`); // .js to .json
return {
...(categories && { categories }),
...(subcategories && { subcategories }),
...(alias && { alias }),
};
}
/**
* Adds a node codex `categories` and `subcategories` (if defined)
* to a node description `codex` property.
*
* @param {object} obj
* @param obj.node Node to add categories to
* @param obj.filePath Path to the built node
* @param obj.isCustom Whether the node is custom
* @returns {void}
*/
addCodex({ node, filePath, isCustom }: {
node: INodeType;
filePath: string;
isCustom: boolean;
}) {
try {
const codex = this.getCodex(filePath);
if (isCustom) {
codex.categories = codex.categories
? codex.categories.concat(CUSTOM_NODES_CATEGORY)
: [CUSTOM_NODES_CATEGORY];
}
node.description.codex = codex;
} catch (_) {
this.logger.debug(`No codex available for: ${filePath.split('/').pop()}`);
if (isCustom) {
node.description.codex = {
categories: [CUSTOM_NODES_CATEGORY],
};
}
}
}
/**
* Loads nodes and credentials from the given directory
@ -209,7 +262,6 @@ class LoadNodesAndCredentialsClass {
* @param {string} setPackageName The package name to set for the found nodes
* @param {string} directory The directory to look in
* @returns {Promise<void>}
* @memberof N8nPackagesInformationClass
*/
async loadDataFromDirectory(setPackageName: string, directory: string): Promise<void> {
const files = await glob(path.join(directory, '**/*\.@(node|credentials)\.js'));
@ -237,7 +289,6 @@ class LoadNodesAndCredentialsClass {
*
* @param {string} packageName The name to read data from
* @returns {Promise<void>}
* @memberof N8nPackagesInformationClass
*/
async loadDataFromPackage(packageName: string): Promise<void> {
// Get the absolute path of the package

View file

@ -93,6 +93,10 @@ export function sendErrorResponse(res: Response, error: ResponseError) {
message: 'Unknown error',
};
if (error.name === 'NodeApiError') {
Object.assign(response, error);
}
if (error.errorCode) {
response.code = error.errorCode;
}

View file

@ -673,11 +673,14 @@ class App {
await WorkflowHelpers.validateWorkflow(updateData);
await Db.collections.Workflow!.update(id, updateData).catch(WorkflowHelpers.throwDuplicateEntryError);
if (tags) {
const tablePrefix = config.get('database.tablePrefix');
await TagHelpers.removeRelations(req.params.id, tablePrefix);
if (tags?.length) {
if (tags.length) {
await TagHelpers.createRelations(req.params.id, tags, tablePrefix);
}
}
// We sadly get nothing back from "update". Neither if it updated a record
// nor the new value. So query now the hopefully updated entry.
@ -944,6 +947,9 @@ class App {
const filepath = nodeType.description.icon.substr(5);
const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
res.setHeader('Cache-control', `private max-age=${maxAge}`);
res.sendFile(filepath);
});

View file

@ -387,7 +387,12 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
}
// Leave log message before flatten as that operation increased memory usage a lot and the chance of a crash is highest here
Logger.debug(`Save execution data to database for execution ID ${this.executionId}`, { executionId: this.executionId, workflowId: this.workflowData.id });
Logger.debug(`Save execution data to database for execution ID ${this.executionId}`, {
executionId: this.executionId,
workflowId: this.workflowData.id,
finished: fullExecutionData.finished,
stoppedAt: fullExecutionData.stoppedAt,
});
const executionData = ResponseHelper.flattenExecutionData(fullExecutionData);
@ -404,6 +409,12 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, this.executionId, this.retryOf);
}
} catch (error) {
Logger.error(`Failed saving execution data to DB on execution ID ${this.executionId}`, {
executionId: this.executionId,
workflowId: this.workflowData.id,
error,
});
if (!isManualMode) {
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf);
}
@ -656,7 +667,7 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi
}
export function sendMessageToUI(source: string, message: string) {
export function sendMessageToUI(source: string, message: any) { // tslint:disable-line:no-any
if (this.sessionId === undefined) {
return;
}

View file

@ -137,7 +137,7 @@ export class WorkflowRunnerProcess {
const additionalData = await WorkflowExecuteAdditionalData.getBase(this.data.credentials, undefined, workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000);
additionalData.hooks = this.getProcessForwardHooks();
additionalData.sendMessageToUI = async (source: string, message: string) => {
additionalData.sendMessageToUI = async (source: string, message: any) => { // tslint:disable-line:no-any
if (workflowRunner.data!.executionMode !== 'manual') {
return;
}

View file

@ -0,0 +1,44 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import config = require('../../../../config');
export class CertifyCorrectCollation1623936588000 implements MigrationInterface {
name = 'CertifyCorrectCollation1623936588000';
async up(queryRunner: QueryRunner): Promise<void> {
const tablePrefix = config.get('database.tablePrefix');
const databaseType = config.get('database.type');
if (databaseType === 'mariadb') {
// This applies to MySQL only.
return;
}
const checkCollationExistence = await queryRunner.query(`show collation where collation like 'utf8mb4_0900_ai_ci';`);
let collation = 'utf8mb4_general_ci';
if (checkCollationExistence.length > 0) {
collation = 'utf8mb4_0900_ai_ci';
}
const databaseName = config.get(`database.mysqldb.database`);
await queryRunner.query(`ALTER DATABASE \`${databaseName}\` CHARACTER SET utf8mb4 COLLATE ${collation};`);
for (const tableName of [
'credentials_entity',
'execution_entity',
'tag_entity',
'webhook_entity',
'workflow_entity',
'workflows_tags',
]) {
await queryRunner.query(`ALTER TABLE ${tablePrefix}${tableName} CONVERT TO CHARACTER SET utf8mb4 COLLATE ${collation};`);
}
}
async down(queryRunner: QueryRunner): Promise<void> {
// There is nothing to undo in this case as we already expect default collation to be utf8mb4
// This migration exists simply to enforce that n8n will work with
// older mysql versions
}
}

View file

@ -7,6 +7,7 @@ import { ChangeDataSize1615306975123 } from './1615306975123-ChangeDataSize';
import { ChangeCredentialDataSize1620729500000 } from './1620729500000-ChangeCredentialDataSize';
import { CreateTagEntity1617268711084 } from './1617268711084-CreateTagEntity';
import { UniqueWorkflowNames1620826335440 } from './1620826335440-UniqueWorkflowNames';
import { CertifyCorrectCollation1623936588000 } from './1623936588000-CertifyCorrectCollation';
export const mysqlMigrations = [
InitialMigration1588157391238,
@ -18,4 +19,5 @@ export const mysqlMigrations = [
ChangeCredentialDataSize1620729500000,
CreateTagEntity1617268711084,
UniqueWorkflowNames1620826335440,
CertifyCorrectCollation1623936588000,
];

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.73.0",
"version": "0.75.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -47,7 +47,7 @@
"file-type": "^14.6.2",
"lodash.get": "^4.4.2",
"mime-types": "^2.1.27",
"n8n-workflow": "~0.60.0",
"n8n-workflow": "~0.62.0",
"oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0",
"request": "^2.88.2",

View file

@ -749,7 +749,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx
return workflow.getStaticData(type, node);
},
prepareOutputData: NodeHelpers.prepareOutputData,
sendMessageToUI(message: string): void {
sendMessageToUI(message: any): void { // tslint:disable-line:no-any
if (mode !== 'manual') {
return;
}

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.92.2",
"version": "0.96.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -40,6 +40,7 @@
"@types/lodash.set": "^4.3.6",
"@types/node": "^14.14.40",
"@types/quill": "^2.0.1",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^2.13.0",
"@typescript-eslint/parser": "^2.13.0",
"@vue/cli-plugin-babel": "^4.1.2",
@ -67,7 +68,7 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.60.0",
"n8n-workflow": "~0.62.0",
"node-sass": "^4.12.0",
"normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1",
@ -78,7 +79,7 @@
"ts-jest": "^26.3.0",
"tslint": "^6.1.2",
"typescript": "~3.9.7",
"uuid": "^8.1.0",
"uuid": "^8.3.0",
"vue": "^2.6.9",
"vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0",
"vue-json-pretty": "1.7.1",
@ -86,7 +87,7 @@
"vue-router": "^3.0.6",
"vue-template-compiler": "^2.5.17",
"vue-typed-mixins": "^0.2.0",
"vue2-touch-events": "^2.3.2",
"vue2-touch-events": "^3.2.1",
"vuex": "^3.1.1"
}
}

View file

@ -221,6 +221,15 @@ export interface IWorkflowDataUpdate {
tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response
}
export interface IWorkflowTemplate {
id: string;
name: string;
workflow: {
nodes: INodeUi[];
connections: IConnections;
};
}
// Almost identical to cli.Interfaces.ts
export interface IWorkflowDb {
id: string;
@ -489,6 +498,39 @@ export interface ILinkMenuItemProperties {
newWindow?: boolean;
}
export interface ISubcategoryItemProps {
subcategory: string;
description: string;
}
export interface INodeItemProps {
subcategory: string;
nodeType: INodeTypeDescription;
}
export interface ICategoryItemProps {
expanded: boolean;
}
export interface INodeCreateElement {
type: 'node' | 'category' | 'subcategory';
category: string;
key: string;
includedByTrigger?: boolean;
includedByRegular?: boolean;
properties: ISubcategoryItemProps | INodeItemProps | ICategoryItemProps;
}
export interface ICategoriesWithNodes {
[category: string]: {
[subcategory: string]: {
regularCount: number;
triggerCount: number;
nodes: INodeCreateElement[];
};
};
}
export interface ITag {
id: string;
name: string;
@ -569,3 +611,8 @@ export interface IRestApiContext {
baseUrl: string;
sessionId: string;
}
export interface IZoomConfig {
scale: number;
offset: XYPositon;
}

View file

@ -42,15 +42,13 @@ class ResponseError extends Error {
}
}
export async function makeRestApiRequest(context: IRestApiContext, method: Method, endpoint: string, data?: IDataObject) {
const { baseUrl, sessionId } = context;
async function request(config: {method: Method, baseURL: string, endpoint: string, headers?: IDataObject, data?: IDataObject}) {
const { method, baseURL, endpoint, headers, data } = config;
const options: AxiosRequestConfig = {
method,
url: endpoint,
baseURL: baseUrl,
headers: {
sessionid: sessionId,
},
baseURL,
headers,
};
if (['PATCH', 'POST', 'PUT'].includes(method)) {
options.data = data;
@ -60,7 +58,7 @@ export async function makeRestApiRequest(context: IRestApiContext, method: Metho
try {
const response = await axios.request(options);
return response.data.data;
return response.data;
} catch (error) {
if (error.message === 'Network Error') {
throw new ResponseError('API-Server can not be reached. It is probably down.');
@ -68,9 +66,31 @@ export async function makeRestApiRequest(context: IRestApiContext, method: Metho
const errorResponseData = error.response.data;
if (errorResponseData !== undefined && errorResponseData.message !== undefined) {
if (errorResponseData.name === 'NodeApiError') {
errorResponseData.httpStatusCode = error.response.status;
throw errorResponseData;
}
throw new ResponseError(errorResponseData.message, {errorCode: errorResponseData.code, httpStatusCode: error.response.status, stack: errorResponseData.stack});
}
throw error;
}
}
export async function makeRestApiRequest(context: IRestApiContext, method: Method, endpoint: string, data?: IDataObject) {
const response = await request({
method,
baseURL: context.baseUrl,
endpoint,
headers: {sessionid: context.sessionId},
data,
});
// @ts-ignore all cli rest api endpoints return data wrapped in `data` key
return response.data;
}
export async function get(baseURL: string, endpoint: string, params?: IDataObject) {
return await request({method: 'GET', baseURL, endpoint, data: params});
}

View file

@ -1,6 +1,11 @@
import { IRestApiContext } from '@/Interface';
import { makeRestApiRequest } from './helpers';
import { IRestApiContext, IWorkflowTemplate } from '@/Interface';
import { makeRestApiRequest, get } from './helpers';
import { TEMPLATES_BASE_URL } from '@/constants';
export async function getNewWorkflow(context: IRestApiContext, name?: string) {
return await makeRestApiRequest(context, 'GET', `/workflows/new`, name ? { name } : {});
}
export async function getWorkflowTemplate(templateId: string): Promise<IWorkflowTemplate> {
return await get(TEMPLATES_BASE_URL, `/workflows/templates/${templateId}`);
}

View file

@ -86,11 +86,9 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
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;
},
},
@ -111,6 +109,7 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
close (e: MouseEvent) {
// @ts-ignore
if (e.target.className && e.target.className.includes && e.target.className.includes('close-on-click')) {
this.$externalHooks().run('dataDisplay.nodeEditingFinished');
this.showDocumentHelp = false;
this.$store.commit('setActiveNode', null);
}

View file

@ -37,10 +37,11 @@
<span class="box-card__subtitle">Data below may contain sensitive information. Proceed with caution when sharing.</span>
</div>
<div>
<el-button class="copy-button" @click="copyCause" circle type="text" title="Copy to clipboard">
<el-button v-if="displayCause" class="copy-button" @click="copyCause" circle type="text" title="Copy to clipboard">
<font-awesome-icon icon="copy" />
</el-button>
<vue-json-pretty
v-if="displayCause"
:data="error.cause"
:deep="3"
:showLength="true"
@ -48,6 +49,9 @@
path="error"
class="json-data"
/>
<span v-else>
<font-awesome-icon icon="info-circle" /> The error cause is too large to be displayed.
</span>
</div>
</el-card>
</div>
@ -67,13 +71,14 @@
</template>
<script lang="ts">
import Vue from 'vue';
//@ts-ignore
import VueJsonPretty from 'vue-json-pretty';
import { copyPaste } from '@/components/mixins/copyPaste';
import { showMessage } from '@/components/mixins/showMessage';
import mixins from 'vue-typed-mixins';
import {
MAX_DISPLAY_DATA_SIZE,
} from '@/constants';
export default mixins(
copyPaste,
@ -86,6 +91,11 @@ export default mixins(
components: {
VueJsonPretty,
},
computed: {
displayCause(): boolean {
return JSON.stringify(this.error.cause).length < MAX_DISPLAY_DATA_SIZE;
},
},
methods: {
copyCause() {
this.copyToClipboard(JSON.stringify(this.error.cause));

View file

@ -30,7 +30,7 @@
</div>
</div>
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :style="nodeIconStyle"/>
<NodeIcon class="node-icon" :nodeType="nodeType" size="60" :style="nodeIconStyle" :shrink="true"/>
</div>
<div class="node-description">
<div class="node-name" :title="data.name">
@ -46,6 +46,7 @@
<script lang="ts">
import Vue from 'vue';
import { externalHooks } from '@/components/mixins/externalHooks';
import { nodeBase } from '@/components/mixins/nodeBase';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
@ -59,7 +60,7 @@ import NodeIcon from '@/components/NodeIcon.vue';
import mixins from 'vue-typed-mixins';
export default mixins(nodeBase, nodeHelpers, workflowHelpers).extend({
export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).extend({
name: 'Node',
components: {
NodeIcon,
@ -152,6 +153,7 @@ export default mixins(nodeBase, nodeHelpers, workflowHelpers).extend({
this.$emit('runWorkflow', this.data.name, 'Node.executeNode');
},
deleteNode () {
this.$externalHooks().run('node.deleteNode', { node: this.data});
Vue.nextTick(() => {
// Wait a tick else vue causes problems because the data is gone
this.$emit('removeNode', this.data.name);

View file

@ -1,85 +0,0 @@
<template>
<div class="node-item clickable" :class="{active: active}" :data-node-name="nodeName" @click="nodeTypeSelected(nodeType)">
<NodeIcon class="node-icon" :nodeType="nodeType" :style="nodeIconStyle" />
<div class="name">
{{nodeType.displayName}}
</div>
<div class="description">
{{nodeType.description}}
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { INodeTypeDescription } from 'n8n-workflow';
import NodeIcon from '@/components/NodeIcon.vue';
export default Vue.extend({
name: 'NodeCreateItem',
components: {
NodeIcon,
},
props: [
'active',
'filter',
'nodeType',
],
computed: {
nodeIconStyle (): object {
return {
color: this.nodeType.defaults.color,
};
},
nodeName (): string {
return this.nodeType.name;
},
},
methods: {
nodeTypeSelected (nodeType: INodeTypeDescription) {
this.$emit('nodeTypeSelected', nodeType.name);
},
},
});
</script>
<style scoped lang="scss">
.node-item {
position: relative;
border-bottom: 1px solid #eee;
background-color: #fff;
padding: 6px;
border-left: 3px solid #fff;
&:hover {
border-left: 3px solid #ccc;
}
}
.active {
border-left: 3px solid $--color-primary;
}
.node-icon {
display: inline-block;
position: absolute;
left: 12px;
top: calc(50% - 15px);
}
.name {
font-weight: bold;
font-size: 0.9em;
padding-left: 50px;
}
.description {
margin-top: 3px;
line-height: 1.7em;
font-size: 0.8em;
padding-left: 50px;
}
</style>

View file

@ -1,172 +0,0 @@
<template>
<div>
<div class="input-wrapper">
<el-input placeholder="Type to filter..." v-model="nodeFilter" ref="inputField" size="small" type="text" prefix-icon="el-icon-search" @keydown.native="nodeFilterKeyDown" clearable ></el-input>
</div>
<div class="type-selector">
<el-tabs v-model="selectedType" stretch>
<el-tab-pane label="Regular" name="Regular"></el-tab-pane>
<el-tab-pane label="Trigger" name="Trigger"></el-tab-pane>
<el-tab-pane label="All" name="All"></el-tab-pane>
</el-tabs>
</div>
<div class="node-create-list-wrapper">
<div class="node-create-list">
<div v-if="filteredNodeTypes.length === 0" class="no-results">
🙃 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>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { externalHooks } from "@/components/mixins/externalHooks";
import { INodeTypeDescription } from 'n8n-workflow';
import NodeCreateItem from '@/components/NodeCreateItem.vue';
import mixins from "vue-typed-mixins";
export default mixins(externalHooks).extend({
name: 'NodeCreateList',
components: {
NodeCreateItem,
},
data () {
return {
activeNodeTypeIndex: 0,
nodeFilter: '',
selectedType: 'Regular',
};
},
computed: {
nodeTypes (): INodeTypeDescription[] {
return this.$store.getters.allNodeTypes;
},
filteredNodeTypes () {
const filter = this.nodeFilter.toLowerCase();
const nodeTypes: INodeTypeDescription[] = this.$store.getters.allNodeTypes;
// Apply the filters
const returnData = nodeTypes.filter((nodeType) => {
if (filter && nodeType.displayName.toLowerCase().indexOf(filter) === -1) {
return false;
}
if (this.selectedType !== 'All') {
if (this.selectedType === 'Trigger' && !nodeType.group.includes('trigger')) {
return false;
} else if (this.selectedType === 'Regular' && nodeType.group.includes('trigger')) {
return false;
}
}
return true;
});
// Sort the node types
let textA, textB;
returnData.sort((a, b) => {
textA = a.displayName.toLowerCase();
textB = b.displayName.toLowerCase();
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
});
this.$externalHooks().run('nodeCreateList.filteredNodeTypesComputed', { nodeFilter: this.nodeFilter, result: returnData, selectedType: this.selectedType });
return returnData;
},
},
watch: {
nodeFilter (newValue, oldValue) {
// Reset the index whenver the filter-value changes
this.activeNodeTypeIndex = 0;
this.$externalHooks().run('nodeCreateList.nodeFilterChanged', { oldValue, newValue, selectedType: this.selectedType, filteredNodes: this.filteredNodeTypes });
},
selectedType (newValue, oldValue) {
this.$externalHooks().run('nodeCreateList.selectedTypeChanged', { oldValue, newValue });
},
},
methods: {
nodeFilterKeyDown (e: KeyboardEvent) {
const activeNodeType = this.filteredNodeTypes[this.activeNodeTypeIndex];
if (e.key === 'ArrowDown') {
this.activeNodeTypeIndex++;
// Make sure that we stop at the last nodeType
this.activeNodeTypeIndex = Math.min(this.activeNodeTypeIndex, this.filteredNodeTypes.length - 1);
} else if (e.key === 'ArrowUp') {
this.activeNodeTypeIndex--;
// Make sure that we do not get before the first nodeType
this.activeNodeTypeIndex = Math.max(this.activeNodeTypeIndex, 0);
} else if (e.key === 'Enter' && activeNodeType) {
this.nodeTypeSelected(activeNodeType.name);
}
if (!['Escape', 'Tab'].includes(e.key)) {
// We only want to propagate "Escape" as it closes the node-creator and
// "Tab" which toggles it
e.stopPropagation();
}
},
nodeTypeSelected (nodeTypeName: string) {
this.$emit('nodeTypeSelected', nodeTypeName);
},
},
async mounted() {
this.$externalHooks().run('nodeCreateList.mounted');
},
async destroyed() {
this.$externalHooks().run('nodeCreateList.destroyed');
},
});
</script>
<style scoped>
.node-create-list-wrapper {
position: absolute;
top: 160px;
left: 0px;
right: 0px;
bottom: 0;
overflow-y: auto;
overflow-x: hidden;
background-color: #fff;
}
.node-create-list {
position: relative;
width: 100%;
}
.group-name {
font-size: 0.9em;
padding: 15px 0 5px 10px;
}
.input-wrapper >>> .el-input__inner,
.input-wrapper >>> .el-input__inner:hover {
background-color: #fff;
}
.input-wrapper {
margin: 10px;
height: 35px;
}
.type-selector {
height: 50px;
text-align: center;
}
.type-selector >>> .el-tabs__nav {
padding-bottom: 10px;
}
.no-results {
margin: 20px 10px 0 10px;
line-height: 1.5em;
text-align: center;
}
</style>

View file

@ -1,104 +0,0 @@
<template>
<div class="node-creator-wrapper">
<transition name="el-zoom-in-top">
<div class="node-creator" v-show="active">
<div class="close-button clickable close-on-click" @click="closeCreator" title="Close">
<i class="el-icon-close close-on-click"></i>
</div>
<div class="header">
Create Node
</div>
<node-create-list v-if="active" ref="list" @nodeTypeSelected="nodeTypeSelected"></node-create-list>
</div>
</transition>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import NodeCreateList from '@/components/NodeCreateList.vue';
export default Vue.extend({
name: 'NodeCreator',
components: {
NodeCreateList,
},
props: [
'active',
],
data () {
return {
};
},
watch: {
active (newValue, oldValue) {
if (newValue === true) {
// Try to set focus directly on the filter-input-field
setTimeout(() => {
// @ts-ignore
if (this.$refs.list && this.$refs.list.$refs.inputField) {
// @ts-ignore
this.$refs.list.$refs.inputField.focus();
}
});
}
},
},
methods: {
closeCreator () {
this.$emit('closeNodeCreator');
},
nodeTypeSelected (nodeTypeName: string) {
this.$emit('nodeTypeSelected', nodeTypeName);
},
},
});
</script>
<style scoped lang="scss">
.close-button {
position: absolute;
top: 0;
left: -50px;
color: #fff;
background-color: $--custom-header-background;
border-radius: 18px 0 0 18px;
z-index: 110;
font-size: 1.7em;
text-align: center;
line-height: 50px;
height: 50px;
width: 50px;
.close-on-click {
color: #fff;
font-weight: 400;
&:hover {
transform: scale(1.2);
}
}
}
.node-creator {
position: fixed;
top: 65px;
right: 0;
width: 350px;
height: calc(100% - 65px);
background-color: #fff4f1;
z-index: 200;
color: #555;
.header {
font-size: 1.2em;
margin: 20px 15px;
height: 25px;
}
}
</style>

View file

@ -0,0 +1,42 @@
<template functional>
<div :class="$style.category">
<span :class="$style.name">{{ props.item.category }}</span>
<font-awesome-icon
:class="$style.arrow"
icon="chevron-down"
v-if="props.item.properties.expanded"
/>
<font-awesome-icon :class="$style.arrow" icon="chevron-up" v-else />
</div>
</template>
<script lang="ts">
export default {
props: ['item'],
};
</script>
<style lang="scss" module>
.category {
font-size: 11px;
font-weight: bold;
letter-spacing: 1px;
line-height: 11px;
padding: 10px 0;
margin: 0 12px;
border-bottom: 1px solid $--node-creator-border-color;
display: flex;
text-transform: uppercase;
}
.name {
flex-grow: 1;
}
.arrow {
font-size: 12px;
width: 12px;
color: $--node-creator-arrow-color;
}
</style>

View file

@ -0,0 +1,57 @@
<template functional>
<div
:class="{
container: true,
clickable: props.clickable,
active: props.active,
}"
@click="listeners['click']"
>
<CategoryItem
v-if="props.item.type === 'category'"
:item="props.item"
/>
<SubcategoryItem
v-else-if="props.item.type === 'subcategory'"
:item="props.item"
/>
<NodeItem
v-else-if="props.item.type === 'node'"
:nodeType="props.item.properties.nodeType"
:bordered="!props.lastNode"
></NodeItem>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import NodeItem from './NodeItem.vue';
import CategoryItem from './CategoryItem.vue';
import SubcategoryItem from './SubcategoryItem.vue';
Vue.component('CategoryItem', CategoryItem);
Vue.component('SubcategoryItem', SubcategoryItem);
Vue.component('NodeItem', NodeItem);
export default {
props: ['item', 'active', 'clickable', 'lastNode'],
};
</script>
<style lang="scss" scoped>
.container {
position: relative;
border-left: 2px solid transparent;
&:hover {
border-color: $--node-creator-item-hover-border-color;
}
&.active {
border-color: $--color-primary !important;
}
}
</style>

View file

@ -0,0 +1,94 @@
<template>
<div>
<div
:is="transitionsEnabled ? 'transition-group' : 'div'"
name="accordion"
@before-enter="beforeEnter"
@enter="enter"
@before-leave="beforeLeave"
@leave="leave"
>
<div v-for="(item, index) in elements" :key="item.key" :class="item.type">
<CreatorItem
:item="item"
:active="activeIndex === index && !disabled"
:clickable="!disabled"
:lastNode="
index === elements.length - 1 || elements[index + 1].type !== 'node'
"
@click="() => selected(item)"
/>
</div>
</div>
</div>
</template>
<script lang="ts">
import { INodeCreateElement } from '@/Interface';
import Vue from 'vue';
import CreatorItem from './CreatorItem.vue';
export default Vue.extend({
name: 'ItemIterator',
components: {
CreatorItem,
},
props: ['elements', 'activeIndex', 'disabled', 'transitionsEnabled'],
methods: {
selected(element: INodeCreateElement) {
if (this.$props.disabled) {
return;
}
this.$emit('selected', element);
},
beforeEnter(el: HTMLElement) {
el.style.height = '0';
},
enter(el: HTMLElement) {
el.style.height = `${el.scrollHeight}px`;
},
beforeLeave(el: HTMLElement) {
el.style.height = `${el.scrollHeight}px`;
},
leave(el: HTMLElement) {
el.style.height = '0';
},
},
});
</script>
<style lang="scss" scoped>
.accordion-enter {
opacity: 0;
}
.accordion-leave-active {
opacity: 1;
}
.accordion-leave-active {
transition: all 0.25s ease, opacity 0.1s ease;
margin-top: 0;
}
.accordion-enter-active {
transition: all 0.25s ease, opacity 0.25s ease;
margin-top: 0;
}
.accordion-leave-to {
opacity: 0;
}
.accordion-enter-to {
opacity: 1;
}
.subcategory + .category,
.node + .category {
margin-top: 15px;
}
</style>

View file

@ -0,0 +1,332 @@
<template>
<div @click="onClickInside" class="container">
<SlideTransition>
<SubcategoryPanel v-if="activeSubcategory" :elements="subcategorizedNodes" :title="activeSubcategory.properties.subcategory" :activeIndex="activeSubcategoryIndex" @close="onSubcategoryClose" @selected="selected" />
</SlideTransition>
<div class="main-panel">
<SearchBar
v-model="nodeFilter"
:eventBus="searchEventBus"
@keydown.native="nodeFilterKeyDown"
/>
<div class="type-selector">
<el-tabs v-model="selectedType" stretch>
<el-tab-pane label="All" :name="ALL_NODE_FILTER"></el-tab-pane>
<el-tab-pane label="Regular" :name="REGULAR_NODE_FILTER"></el-tab-pane>
<el-tab-pane label="Trigger" :name="TRIGGER_NODE_FILTER"></el-tab-pane>
</el-tabs>
</div>
<div v-if="searchFilter.length === 0" class="scrollable">
<ItemIterator
:elements="categorized"
:disabled="!!activeSubcategory"
:activeIndex="activeIndex"
:transitionsEnabled="true"
@selected="selected"
/>
</div>
<div
class="scrollable"
v-else-if="filteredNodeTypes.length > 0"
>
<ItemIterator
:elements="filteredNodeTypes"
:activeIndex="activeIndex"
@selected="selected"
/>
</div>
<NoResults v-else @nodeTypeSelected="nodeTypeSelected" />
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { externalHooks } from '@/components/mixins/externalHooks';
import mixins from 'vue-typed-mixins';
import ItemIterator from './ItemIterator.vue';
import NoResults from './NoResults.vue';
import SearchBar from './SearchBar.vue';
import SubcategoryPanel from './SubcategoryPanel.vue';
import { INodeCreateElement, INodeItemProps, ISubcategoryItemProps } from '@/Interface';
import { ALL_NODE_FILTER, CORE_NODES_CATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER } from '@/constants';
import SlideTransition from '../transitions/SlideTransition.vue';
import { matchesNodeType, matchesSelectType } from './helpers';
export default mixins(externalHooks).extend({
name: 'NodeCreateList',
components: {
ItemIterator,
NoResults,
SubcategoryPanel,
SlideTransition,
SearchBar,
},
props: ['categorizedItems', 'categoriesWithNodes', 'searchItems'],
data() {
return {
activeCategory: [] as string[],
activeSubcategory: null as INodeCreateElement | null,
activeIndex: 1,
activeSubcategoryIndex: 0,
nodeFilter: '',
selectedType: ALL_NODE_FILTER,
searchEventBus: new Vue(),
REGULAR_NODE_FILTER,
TRIGGER_NODE_FILTER,
ALL_NODE_FILTER,
};
},
computed: {
searchFilter(): string {
return this.nodeFilter.toLowerCase().trim();
},
filteredNodeTypes(): INodeCreateElement[] {
const nodeTypes: INodeCreateElement[] = this.searchItems;
const filter = this.searchFilter;
const returnData = nodeTypes.filter((el: INodeCreateElement) => {
const nodeType = (el.properties as INodeItemProps).nodeType;
return filter && matchesSelectType(el, this.selectedType) && matchesNodeType(el, filter);
});
setTimeout(() => {
this.$externalHooks().run('nodeCreateList.filteredNodeTypesComputed', {
nodeFilter: this.nodeFilter,
result: returnData,
selectedType: this.selectedType,
});
}, 0);
return returnData;
},
categorized() {
return this.categorizedItems && this.categorizedItems
.reduce((accu: INodeCreateElement[], el: INodeCreateElement) => {
if (
el.type !== 'category' &&
!this.activeCategory.includes(el.category)
) {
return accu;
}
if (!matchesSelectType(el, this.selectedType)) {
return accu;
}
if (el.type === 'category') {
accu.push({
...el,
properties: {
expanded: this.activeCategory.includes(el.category),
},
} as INodeCreateElement);
return accu;
}
accu.push(el);
return accu;
}, []);
},
subcategorizedNodes() {
const activeSubcategory = this.activeSubcategory as INodeCreateElement;
const category = activeSubcategory.category;
const subcategory = (activeSubcategory.properties as ISubcategoryItemProps).subcategory;
return activeSubcategory && this.categoriesWithNodes[category][subcategory]
.nodes.filter((el: INodeCreateElement) => matchesSelectType(el, this.selectedType));
},
},
watch: {
nodeFilter(newValue, oldValue) {
// Reset the index whenver the filter-value changes
this.activeIndex = 0;
this.$externalHooks().run('nodeCreateList.nodeFilterChanged', {
oldValue,
newValue,
selectedType: this.selectedType,
filteredNodes: this.filteredNodeTypes,
});
},
selectedType(newValue, oldValue) {
this.$externalHooks().run('nodeCreateList.selectedTypeChanged', {
oldValue,
newValue,
});
},
},
methods: {
nodeFilterKeyDown(e: KeyboardEvent) {
if (!['Escape', 'Tab'].includes(e.key)) {
// We only want to propagate 'Escape' as it closes the node-creator and
// 'Tab' which toggles it
e.stopPropagation();
}
if (this.activeSubcategory) {
const activeList = this.subcategorizedNodes;
const activeNodeType = activeList[this.activeSubcategoryIndex];
if (e.key === 'ArrowDown' && this.activeSubcategory) {
this.activeSubcategoryIndex++;
this.activeSubcategoryIndex = Math.min(
this.activeSubcategoryIndex,
activeList.length - 1,
);
}
else if (e.key === 'ArrowUp' && this.activeSubcategory) {
this.activeSubcategoryIndex--;
this.activeSubcategoryIndex = Math.max(this.activeSubcategoryIndex, 0);
}
else if (e.key === 'Enter') {
this.selected(activeNodeType);
}
else if (e.key === 'ArrowLeft') {
this.onSubcategoryClose();
}
return;
}
let activeList;
if (this.searchFilter.length > 0) {
activeList = this.filteredNodeTypes;
} else {
activeList = this.categorized;
}
const activeNodeType = activeList[this.activeIndex];
if (e.key === 'ArrowDown') {
this.activeIndex++;
// Make sure that we stop at the last nodeType
this.activeIndex = Math.min(
this.activeIndex,
activeList.length - 1,
);
} else if (e.key === 'ArrowUp') {
this.activeIndex--;
// Make sure that we do not get before the first nodeType
this.activeIndex = Math.max(this.activeIndex, 0);
} else if (e.key === 'Enter' && activeNodeType) {
this.selected(activeNodeType);
} else if (e.key === 'ArrowRight' && activeNodeType.type === 'subcategory') {
this.selected(activeNodeType);
} else if (e.key === 'ArrowRight' && activeNodeType.type === 'category' && !activeNodeType.properties.expanded) {
this.selected(activeNodeType);
} else if (e.key === 'ArrowLeft' && activeNodeType.type === 'category' && activeNodeType.properties.expanded) {
this.selected(activeNodeType);
}
},
selected(element: INodeCreateElement) {
if (element.type === 'node') {
const properties = element.properties as INodeItemProps;
this.nodeTypeSelected(properties.nodeType.name);
} else if (element.type === 'category') {
this.onCategorySelected(element.category);
} else if (element.type === 'subcategory') {
this.onSubcategorySelected(element);
}
},
nodeTypeSelected(nodeTypeName: string) {
this.$emit('nodeTypeSelected', nodeTypeName);
},
onCategorySelected(category: string) {
if (this.activeCategory.includes(category)) {
this.activeCategory = this.activeCategory.filter(
(active: string) => active !== category,
);
} else {
this.activeCategory = [...this.activeCategory, category];
}
this.activeIndex = this.categorized.findIndex(
(el: INodeCreateElement) => el.category === category,
);
},
onSubcategorySelected(selected: INodeCreateElement) {
this.activeSubcategoryIndex = 0;
this.activeSubcategory = selected;
},
onSubcategoryClose() {
this.activeSubcategory = null;
this.activeSubcategoryIndex = 0;
this.nodeFilter = '';
},
onClickInside() {
this.searchEventBus.$emit('focus');
},
},
async mounted() {
this.$nextTick(() => {
// initial opening effect
this.activeCategory = [CORE_NODES_CATEGORY];
});
this.$externalHooks().run('nodeCreateList.mounted');
},
async destroyed() {
this.$externalHooks().run('nodeCreateList.destroyed');
},
});
</script>
<style lang="scss" scoped>
/deep/ .el-tabs__item {
padding: 0;
}
/deep/ .el-tabs__active-bar {
height: 1px;
}
/deep/ .el-tabs__nav-wrap::after {
height: 1px;
}
.container {
height: 100%;
> div {
height: 100%;
}
}
.main-panel .scrollable {
height: calc(100% - 160px);
padding-top: 1px;
}
.scrollable {
overflow-y: auto;
overflow-x: visible;
&::-webkit-scrollbar {
display: none;
}
> div {
padding-bottom: 30px;
}
}
.type-selector {
text-align: center;
background-color: $--node-creator-select-background-color;
/deep/ .el-tabs > div {
margin-bottom: 0;
.el-tabs__nav {
height: 43px;
}
}
}
</style>

View file

@ -0,0 +1,126 @@
<template>
<div class="no-results">
<div class="icon">
<NoResultsIcon />
</div>
<div class="title">
<div>We didn't make that... yet</div>
<div class="action">
Dont worry, you can probably do it with the
<a @click="selectHttpRequest">HTTP Request</a> or
<a @click="selectWebhook">Webhook</a> node
</div>
</div>
<div class="request">
<div>Want us to make it faster?</div>
<div>
<a
:href="REQUEST_NODE_FORM_URL"
target="_blank"
>
<span>Request the node</span>&nbsp;
<span>
<font-awesome-icon
class="external"
icon="external-link-alt"
title="Request the node"
/>
</span>
</a>
</div>
</div>
</div>
</template>
<script lang="ts">
import { HTTP_REQUEST_NODE_NAME, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_NAME } from '@/constants';
import Vue from 'vue';
import NoResultsIcon from './NoResultsIcon.vue';
export default Vue.extend({
name: 'NoResults',
components: {
NoResultsIcon,
},
data() {
return {
REQUEST_NODE_FORM_URL,
};
},
computed: {
basePath(): string {
return this.$store.getters.getBaseUrl;
},
},
methods: {
selectWebhook() {
this.$emit('nodeTypeSelected', WEBHOOK_NODE_NAME);
},
selectHttpRequest() {
this.$emit('nodeTypeSelected', HTTP_REQUEST_NODE_NAME);
},
},
});
</script>
<style lang="scss" scoped>
.no-results {
background-color: $--node-creator-no-results-background-color;
text-align: center;
height: 100%;
border-left: 1px solid $--node-creator-border-color;
flex-direction: column;
font-weight: 400;
display: flex;
align-items: center;
align-content: center;
padding: 0 50px;
}
.title {
font-size: 22px;
line-height: 22px;
margin-top: 50px;
div {
margin-bottom: 15px;
}
}
.action, .request {
font-size: 14px;
line-height: 19px;
}
.request {
position: fixed;
bottom: 20px;
display: none;
@media (min-height: 550px) {
display: block;
}
}
a {
color: $--color-primary;
text-decoration: none;
cursor: pointer;
font-weight: 500;
}
.icon {
margin-top: 100px;
min-height: 67px;
opacity: .6;
}
.external {
font-size: 12px;
}
</style>

View file

@ -0,0 +1,22 @@
<template>
<svg width="75px" height="75px" viewBox="0 0 75 75" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>no-nodes-keyart</title>
<g id="Nodes-panel-prototype-V2.1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="nodes-panel-(component)" transform="translate(-2085.000000, -352.000000)">
<g id="nodes_panel" transform="translate(1880.000000, 151.000000)">
<g id="Panel" transform="translate(50.000000, 0.000000)">
<g id="Group-3" transform="translate(105.000000, 171.000000)">
<g id="no-nodes-keyart" transform="translate(50.000000, 30.000000)">
<rect id="Rectangle" x="0" y="0" width="75" height="75"></rect>
<g id="Group" transform="translate(6.562500, 8.164062)" fill="#C4C8D1" fill-rule="nonzero">
<polygon id="Rectangle" transform="translate(49.192016, 45.302553) rotate(-45.000000) translate(-49.192016, -45.302553) " points="44.5045606 32.0526802 53.8794707 32.0526802 53.8794707 58.5524261 44.5045606 58.5524261"></polygon>
<path d="M48.125,23.0859375 C54.15625,23.0859375 59.0625,18.1796875 59.0625,12.1484375 C59.0625,10.3359375 58.5625,8.6484375 57.78125,7.1484375 L49.34375,15.5859375 L44.6875,10.9296875 L53.125,2.4921875 C51.625,1.7109375 49.9375,1.2109375 48.125,1.2109375 C42.09375,1.2109375 37.1875,6.1171875 37.1875,12.1484375 C37.1875,13.4296875 37.4375,14.6484375 37.84375,15.7734375 L32.0625,21.5546875 L26.5,15.9921875 L28.71875,13.7734375 L24.3125,9.3671875 L30.9375,2.7421875 C27.28125,-0.9140625 21.34375,-0.9140625 17.6875,2.7421875 L6.625,13.8046875 L11.03125,18.2109375 L2.21875,18.2109375 L1.38777878e-15,20.4296875 L11.0625,31.4921875 L13.28125,29.2734375 L13.28125,20.4296875 L17.6875,24.8359375 L19.90625,22.6171875 L25.46875,28.1796875 L2.3125,51.3359375 L8.9375,57.9609375 L44.5,22.4296875 C45.625,22.8359375 46.84375,23.0859375 48.125,23.0859375 Z" id="Path"></path>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>
</template>

View file

@ -0,0 +1,101 @@
<template>
<div>
<SlideTransition>
<div class="node-creator" v-if="active" v-click-outside="closeCreator">
<MainPanel @nodeTypeSelected="nodeTypeSelected" :categorizedItems="categorizedItems" :categoriesWithNodes="categoriesWithNodes" :searchItems="searchItems"></MainPanel>
</div>
</SlideTransition>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { ICategoriesWithNodes, INodeCreateElement } from '@/Interface';
import { INodeTypeDescription } from 'n8n-workflow';
import SlideTransition from '../transitions/SlideTransition.vue';
import { HIDDEN_NODES } from '@/constants';
import MainPanel from './MainPanel.vue';
import { getCategoriesWithNodes, getCategorizedList } from './helpers';
export default Vue.extend({
name: 'NodeCreator',
components: {
MainPanel,
SlideTransition,
},
props: [
'active',
],
computed: {
visibleNodeTypes(): INodeTypeDescription[] {
return this.$store.getters.allNodeTypes
.filter((nodeType: INodeTypeDescription) => {
return !HIDDEN_NODES.includes(nodeType.name);
});
},
categoriesWithNodes(): ICategoriesWithNodes {
return getCategoriesWithNodes(this.visibleNodeTypes);
},
categorizedItems(): INodeCreateElement[] {
return getCategorizedList(this.categoriesWithNodes);
},
searchItems(): INodeCreateElement[] {
const sorted = [...this.visibleNodeTypes];
sorted.sort((a, b) => {
const textA = a.displayName.toLowerCase();
const textB = b.displayName.toLowerCase();
return textA < textB ? -1 : textA > textB ? 1 : 0;
});
return sorted.map((nodeType) => ({
type: 'node',
category: '',
key: `${nodeType.name}`,
properties: {
nodeType,
subcategory: '',
},
includedByTrigger: nodeType.group.includes('trigger'),
includedByRegular: !nodeType.group.includes('trigger'),
}));
},
},
methods: {
closeCreator () {
this.$emit('closeNodeCreator');
},
nodeTypeSelected (nodeTypeName: string) {
this.$emit('nodeTypeSelected', nodeTypeName);
},
},
});
</script>
<style scoped lang="scss">
/deep/ *, *:before, *:after {
box-sizing: border-box;
}
.node-creator {
position: fixed;
top: $--header-height;
right: 0;
width: $--node-creator-width;
height: 100%;
background-color: $--node-creator-background-color;
z-index: 200;
color: $--node-creator-text-color;
&:before {
box-sizing: border-box;
content: ' ';
border-left: 1px solid $--node-creator-border-color;
width: 1px;
position: absolute;
height: 100%;
}
}
</style>

View file

@ -0,0 +1,86 @@
<template functional>
<div :class="{[$style['node-item']]: true, [$style.bordered]: props.bordered}">
<NodeIcon :class="$style['node-icon']" :nodeType="props.nodeType" :style="{color: props.nodeType.defaults.color}" />
<div>
<div :class="$style.details">
<span :class="$style.name">{{props.nodeType.displayName}}</span>
<span :class="$style['trigger-icon']">
<TriggerIcon v-if="$options.isTrigger(props.nodeType)" />
</span>
</div>
<div :class="$style.description">
{{props.nodeType.description}}
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { INodeTypeDescription } from 'n8n-workflow';
import NodeIcon from '../NodeIcon.vue';
import TriggerIcon from '../TriggerIcon.vue';
Vue.component('NodeIcon', NodeIcon);
Vue.component('TriggerIcon', TriggerIcon);
export default {
props: [
'active',
'filter',
'nodeType',
'bordered',
],
isTrigger (nodeType: INodeTypeDescription): boolean {
return nodeType.group.includes('trigger');
},
};
</script>
<style lang="scss" module>
.node-item {
padding: 11px 8px 11px 0;
margin-left: 15px;
margin-right: 12px;
display: flex;
&.bordered {
border-bottom: 1px solid $--node-creator-border-color;
}
}
.details {
display: flex;
align-items: center;
}
.node-icon {
min-width: 26px;
max-width: 26px;
margin-right: 15px;
}
.name {
font-weight: bold;
font-size: 14px;
line-height: 18px;
margin-right: 5px;
}
.description {
margin-top: 2px;
font-size: 11px;
line-height: 16px;
font-weight: 400;
color: $--node-creator-description-color;
}
.trigger-icon {
height: 16px;
width: 16px;
display: flex;
}
</style>

View file

@ -0,0 +1,124 @@
<template>
<div class="search-container">
<div :class="{ prefix: true, active: value.length > 0 }">
<font-awesome-icon icon="search" />
</div>
<div class="text">
<input
placeholder="Search nodes..."
ref="input"
:value="value"
@input="onInput"
/>
</div>
<div class="suffix" v-if="value.length > 0" @click="clear">
<span class="clear el-icon-close clickable"></span>
</div>
</div>
</template>
<script lang="ts">
import Vue from "vue";
export default Vue.extend({
name: "SearchBar",
props: ["value", "eventBus"],
mounted() {
if (this.$props.eventBus) {
this.$props.eventBus.$on("focus", () => {
this.focus();
});
}
setTimeout(() => {
this.focus();
}, 0);
},
methods: {
focus() {
const input = this.$refs.input as HTMLInputElement;
if (input) {
input.focus();
}
},
onInput(event: InputEvent) {
const input = event.target as HTMLInputElement;
this.$emit("input", input.value);
},
clear() {
this.$emit("input", "");
},
},
});
</script>
<style lang="scss" scoped>
.search-container {
display: flex;
height: 60px;
align-items: center;
padding-left: 14px;
padding-right: 20px;
border-top: 1px solid $--node-creator-border-color;
border-bottom: 1px solid $--node-creator-border-color;
background-color: $--node-creator-search-background-color;
color: $--node-creator-search-placeholder-color;
}
.prefix {
text-align: center;
font-size: 16px;
margin-right: 14px;
&.active {
color: $--color-primary !important;
}
}
.text {
flex-grow: 1;
input {
width: 100%;
border: none !important;
outline: none;
font-size: 18px;
-webkit-appearance: none;
&::placeholder,
&::-webkit-input-placeholder {
color: $--node-creator-search-placeholder-color;
}
}
}
.suffix {
min-width: 20px;
text-align: center;
display: inline-block;
}
.clear {
background-color: $--node-creator-search-clear-background-color;
border-radius: 50%;
height: 16px;
width: 16px;
font-size: 16px;
color: $--node-creator-search-background-color;
display: inline-flex;
align-items: center;
&:hover {
background-color: $--node-creator-search-clear-background-color-hover;
}
&:before {
line-height: 16px;
display: flex;
height: 16px;
width: 16px;
font-size: 15px;
align-items: center;
justify-content: center;
}
}
</style>

View file

@ -0,0 +1,58 @@
<template functional>
<div :class="$style.subcategory">
<div :class="$style.details">
<div :class="$style.title">{{ props.item.properties.subcategory }}</div>
<div v-if="props.item.properties.description" :class="$style.description">
{{ props.item.properties.description }}
</div>
</div>
<div :class="$style.action">
<font-awesome-icon :class="$style.arrow" icon="arrow-right" />
</div>
</div>
</template>
<script lang="ts">
export default {
props: ['item'],
};
</script>
<style lang="scss" module>
.subcategory {
display: flex;
padding: 11px 16px 11px 30px;
}
.details {
flex-grow: 1;
margin-right: 4px;
}
.title {
font-size: 14px;
font-weight: bold;
line-height: 16px;
margin-bottom: 3px;
}
.description {
font-size: 11px;
line-height: 16px;
font-weight: 400;
color: $--node-creator-description-color;
}
.action {
display: flex;
align-items: center;
}
.arrow {
font-size: 12px;
width: 12px;
color: $--node-creator-arrow-color;
}
</style>

View file

@ -0,0 +1,96 @@
<template>
<div class="subcategory-panel">
<div class="subcategory-header">
<div class="clickable" @click="onBackArrowClick">
<font-awesome-icon class="back-arrow" icon="arrow-left" />
</div>
<span>{{ title }}</span>
</div>
<div class="scrollable">
<ItemIterator
:elements="elements"
:activeIndex="activeIndex"
@selected="selected"
/>
</div>
</div>
</template>
<script lang="ts">
import { INodeCreateElement } from '@/Interface';
import Vue from 'vue';
import ItemIterator from './ItemIterator.vue';
export default Vue.extend({
name: 'SubcategoryPanel',
components: {
ItemIterator,
},
props: ['title', 'elements', 'activeIndex'],
methods: {
selected(element: INodeCreateElement) {
this.$emit('selected', element);
},
onBackArrowClick() {
this.$emit('close');
},
},
});
</script>
<style lang="scss" scoped>
.subcategory-panel {
position: absolute;
background: $--node-creator-search-background-color;
z-index: 100;
height: 100%;
width: 100%;
&:before {
box-sizing: border-box;
content: ' ';
border-left: 1px solid $--node-creator-border-color;
width: 1px;
position: absolute;
height: 100%;
}
}
.subcategory-header {
border: $--node-creator-border-color solid 1px;
height: 50px;
background-color: $--node-creator-subcategory-panel-header-bacground-color;
font-size: 18px;
font-weight: 600;
line-height: 16px;
display: flex;
align-items: center;
padding: 11px 15px;
}
.back-arrow {
color: $--node-creator-arrow-color;
height: 16px;
width: 16px;
margin-right: 24px;
}
.scrollable {
overflow-y: auto;
overflow-x: visible;
height: calc(100% - 100px);
&::-webkit-scrollbar {
display: none;
}
> div {
padding-bottom: 30px;
}
}
</style>

View file

@ -0,0 +1,176 @@
import { CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, SUBCATEGORY_DESCRIPTIONS, UNCATEGORIZED_CATEGORY, UNCATEGORIZED_SUBCATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER, ALL_NODE_FILTER } from '@/constants';
import { INodeCreateElement, ICategoriesWithNodes, INodeItemProps } from '@/Interface';
import { INodeTypeDescription } from 'n8n-workflow';
export const getCategoriesWithNodes = (nodeTypes: INodeTypeDescription[]): ICategoriesWithNodes => {
return nodeTypes.reduce(
(accu: ICategoriesWithNodes, nodeType: INodeTypeDescription) => {
if (!nodeType.codex || !nodeType.codex.categories) {
accu[UNCATEGORIZED_CATEGORY][UNCATEGORIZED_SUBCATEGORY].nodes.push({
type: 'node',
category: UNCATEGORIZED_CATEGORY,
key: `${UNCATEGORIZED_CATEGORY}_${nodeType.name}`,
properties: {
subcategory: UNCATEGORIZED_SUBCATEGORY,
nodeType,
},
includedByTrigger: nodeType.group.includes('trigger'),
includedByRegular: !nodeType.group.includes('trigger'),
});
return accu;
}
nodeType.codex.categories.forEach((_category: string) => {
const category = _category.trim();
const subcategory =
nodeType.codex &&
nodeType.codex.subcategories &&
nodeType.codex.subcategories[category]
? nodeType.codex.subcategories[category][0]
: UNCATEGORIZED_SUBCATEGORY;
if (!accu[category]) {
accu[category] = {};
}
if (!accu[category][subcategory]) {
accu[category][subcategory] = {
triggerCount: 0,
regularCount: 0,
nodes: [],
};
}
const isTrigger = nodeType.group.includes('trigger');
if (isTrigger) {
accu[category][subcategory].triggerCount++;
}
if (!isTrigger) {
accu[category][subcategory].regularCount++;
}
accu[category][subcategory].nodes.push({
type: 'node',
key: `${category}_${nodeType.name}`,
category,
properties: {
nodeType,
subcategory,
},
includedByTrigger: isTrigger,
includedByRegular: !isTrigger,
});
});
return accu;
},
{
[UNCATEGORIZED_CATEGORY]: {
[UNCATEGORIZED_SUBCATEGORY]: {
triggerCount: 0,
regularCount: 0,
nodes: [],
},
},
},
);
};
const getCategories = (categoriesWithNodes: ICategoriesWithNodes): string[] => {
const categories = Object.keys(categoriesWithNodes);
const sorted = categories.filter(
(category: string) =>
category !== CORE_NODES_CATEGORY && category !== CUSTOM_NODES_CATEGORY && category !== UNCATEGORIZED_CATEGORY,
);
sorted.sort();
return [CORE_NODES_CATEGORY, CUSTOM_NODES_CATEGORY, ...sorted, UNCATEGORIZED_CATEGORY];
};
export const getCategorizedList = (categoriesWithNodes: ICategoriesWithNodes): INodeCreateElement[] => {
const categories = getCategories(categoriesWithNodes);
return categories.reduce(
(accu: INodeCreateElement[], category: string) => {
if (!categoriesWithNodes[category]) {
return accu;
}
const categoryEl: INodeCreateElement = {
type: 'category',
key: category,
category,
properties: {
expanded: false,
},
};
const subcategories = Object.keys(categoriesWithNodes[category]);
if (subcategories.length === 1) {
const subcategory = categoriesWithNodes[category][
subcategories[0]
];
if (subcategory.triggerCount > 0) {
categoryEl.includedByTrigger = subcategory.triggerCount > 0;
}
if (subcategory.regularCount > 0) {
categoryEl.includedByRegular = subcategory.regularCount > 0;
}
return [...accu, categoryEl, ...subcategory.nodes];
}
subcategories.sort();
const subcategorized = subcategories.reduce(
(accu: INodeCreateElement[], subcategory: string) => {
const subcategoryEl: INodeCreateElement = {
type: 'subcategory',
key: `${category}_${subcategory}`,
category,
properties: {
subcategory,
description: SUBCATEGORY_DESCRIPTIONS[category][subcategory],
},
includedByTrigger: categoriesWithNodes[category][subcategory].triggerCount > 0,
includedByRegular: categoriesWithNodes[category][subcategory].regularCount > 0,
};
if (subcategoryEl.includedByTrigger) {
categoryEl.includedByTrigger = true;
}
if (subcategoryEl.includedByRegular) {
categoryEl.includedByRegular = true;
}
accu.push(subcategoryEl);
return accu;
},
[],
);
return [...accu, categoryEl, ...subcategorized];
},
[],
);
};
export const matchesSelectType = (el: INodeCreateElement, selectedType: string) => {
if (selectedType === REGULAR_NODE_FILTER && el.includedByRegular) {
return true;
}
if (selectedType === TRIGGER_NODE_FILTER && el.includedByTrigger) {
return true;
}
return selectedType === ALL_NODE_FILTER;
};
const matchesAlias = (nodeType: INodeTypeDescription, filter: string): boolean => {
if (!nodeType.codex || !nodeType.codex.alias) {
return false;
}
return nodeType.codex.alias.reduce((accu: boolean, alias: string) => {
return accu || alias.toLowerCase().indexOf(filter) > -1;
}, false);
};
export const matchesNodeType = (el: INodeCreateElement, filter: string) => {
const nodeType = (el.properties as INodeItemProps).nodeType;
return nodeType.displayName.toLowerCase().indexOf(filter) !== -1 || matchesAlias(nodeType, filter);
};

View file

@ -1,7 +1,7 @@
<template>
<div class="node-icon-wrapper" :style="iconStyleData" :class="{full: isSvgIcon}">
<div class="node-icon-wrapper" :style="iconStyleData" :class="{shrink: isSvgIcon && shrink, full: !shrink}">
<div v-if="nodeIconData !== null" class="icon">
<img :src="nodeIconData.path" style="width: 100%; height: 100%;" v-if="nodeIconData.type === 'file'"/>
<img :src="nodeIconData.path" style="max-width: 100%; max-height: 100%;" v-if="nodeIconData.type === 'file'"/>
<font-awesome-icon :icon="nodeIconData.path" v-else-if="nodeIconData.type === 'fa'" />
</div>
<div v-else class="node-icon-placeholder">
@ -25,6 +25,7 @@ export default Vue.extend({
props: [
'nodeType',
'size',
'shrink',
],
computed: {
iconStyleData (): object {
@ -79,19 +80,23 @@ export default Vue.extend({
<style lang="scss">
.node-icon-wrapper {
width: 30px;
height: 30px;
border-radius: 15px;
width: 26px;
height: 26px;
border-radius: 4px;
color: #444;
line-height: 30px;
line-height: 26px;
font-size: 1.1em;
overflow: hidden;
background-color: #fff;
text-align: center;
font-weight: bold;
font-size: 20px;
&.full .icon {
height: 100%;
width: 100%;
}
&.shrink .icon {
margin: 0.24em;
}

View file

@ -0,0 +1,42 @@
<template functional>
<span :class="$style.trigger">
<svg width="36px" height="36px" viewBox="0 0 36 36" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Trigger node</title>
<g id="/integrations-(V1-feature)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Individual-node-view" transform="translate(-304.000000, -137.000000)" fill-rule="nonzero">
<g id="left-column" transform="translate(120.000000, 131.000000)">
<g id="trigger-badge" transform="translate(178.000000, 0.000000)">
<g id="trigger-icon" transform="translate(6.857143, 6.857143)">
<g id="Icon" transform="translate(8.571429, 0.000000)" fill="#FF6150">
<polygon id="Icon-Path" points="7.14285714 21.4285714 0 21.4285714 10 1.42857143 10 12.8571429 17.1428571 12.8571429 7.14285714 32.8571429"></polygon>
</g>
<rect id="ViewBox" x="0" y="0" width="34.2857143" height="34.2857143"></rect>
</g>
</g>
</g>
</g>
</g>
</svg>
</span>
</template>
<script lang="ts">
export default {
name: 'TriggerIcon',
};
</script>
<style lang="scss" module>
.trigger {
background-color: $--trigger-icon-background-color;
border: 1px solid $--trigger-icon-border-color;
border-radius: 4px;
display: inline-flex;
> svg {
width: 100%;
height: 100%;
}
}
</style>

View file

@ -68,6 +68,9 @@ export const genericHelpers = mixins(showMessage).extend({
},
);
},
setLoadingText (text: string) {
this.loadingService.text = text;
},
stopLoading () {
if (this.loadingService !== null) {
this.loadingService.close();

View file

@ -1,13 +1,9 @@
import Vue from 'vue';
import { Notification } from 'element-ui';
import { ElNotificationOptions } from 'element-ui/types/notification';
import mixins from 'vue-typed-mixins';
import { externalHooks } from '@/components/mixins/externalHooks';
// export const showMessage = {
export const showMessage = mixins(externalHooks).extend({
methods: {
$showMessage(messageData: ElNotificationOptions) {
@ -18,14 +14,47 @@ export const showMessage = mixins(externalHooks).extend({
return Notification(messageData);
},
$showError (error: Error, title: string, message: string) {
$showError(error: Error, title: string, message?: string) {
const messageLine = message ? `${message}<br/>` : '';
this.$showMessage({
title,
message: `${message}<br /><i>${error.message}</i>`,
message: `
${messageLine}
<i>${error.message}</i>
${this.collapsableDetails(error)}`,
type: 'error',
duration: 0,
});
this.$externalHooks().run('showMessage.showError', { title, message, errorMessage: error.message });
this.$externalHooks().run('showMessage.showError', {
title,
message,
errorMessage: error.message,
});
},
// @ts-ignore
collapsableDetails({ description, node }: Error) {
if (!description) return '';
const errorDescription =
description.length > 500
? `${description.slice(0, 500)}...`
: description;
return `
<br>
<br>
<details>
<summary
style="color: #ff6d5a; font-weight: bold; cursor: pointer;"
>
Show Details
</summary>
<p>${node.name}: ${errorDescription}</p>
</details>
`;
},
},
});

View file

@ -0,0 +1,24 @@
<template>
<transition name="slide">
<slot></slot>
</transition>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
name: 'SlideTransition',
});
</script>
<style lang="scss" scoped>
.slide-leave-active,
.slide-enter-active {
transition: 0.3s ease;
}
.slide-leave-to,
.slide-enter {
transform: translateX(100%);
}
</style>

View file

@ -15,8 +15,36 @@ export const DUPLICATE_MODAL_KEY = 'duplicate';
export const TAGS_MANAGER_MODAL_KEY = 'tagsManager';
export const WORKLOW_OPEN_MODAL_KEY = 'workflowOpen';
// breakpoints
export const BREAKPOINT_SM = 768;
export const BREAKPOINT_MD = 992;
export const BREAKPOINT_LG = 1200;
export const BREAKPOINT_XL = 1920;
// templates
export const TEMPLATES_BASE_URL = `https://api.n8n.io/`;
export const START_NODE_TYPE = 'n8n-nodes-base.start';
// Node creator
export const CORE_NODES_CATEGORY = 'Core Nodes';
export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
export const SUBCATEGORY_DESCRIPTIONS: {
[category: string]: { [subcategory: string]: string };
} = {
'Core Nodes': {
Flow: 'Branches, core triggers, merge data',
Files: 'Work with CSV, XML, text, images etc.',
'Data Transformation': 'Manipulate data fields, run code',
Helpers: 'HTTP Requests (API calls), date and time, scrape HTML',
},
};
export const REGULAR_NODE_FILTER = 'Regular';
export const TRIGGER_NODE_FILTER = 'Trigger';
export const ALL_NODE_FILTER = 'All';
export const UNCATEGORIZED_CATEGORY = 'Miscellaneous';
export const UNCATEGORIZED_SUBCATEGORY = 'Helpers';
export const HIDDEN_NODES = ['n8n-nodes-base.start'];
export const WEBHOOK_NODE_NAME = 'n8n-nodes-base.webhook';
export const HTTP_REQUEST_NODE_NAME = 'n8n-nodes-base.httpRequest';
export const REQUEST_NODE_FORM_URL = 'https://n8n-community.typeform.com/to/K1fBVTZ3';

View file

@ -28,12 +28,15 @@ import {
faAngleDown,
faAngleRight,
faAngleUp,
faArrowLeft,
faArrowRight,
faAt,
faBook,
faBug,
faCalendar,
faCheck,
faChevronDown,
faChevronUp,
faCode,
faCodeBranch,
faCog,
@ -48,6 +51,7 @@ import {
faEnvelope,
faEye,
faExclamationTriangle,
faExpand,
faExternalLinkAlt,
faExchangeAlt,
faFile,
@ -79,6 +83,7 @@ import {
faRedo,
faRss,
faSave,
faSearch,
faSearchMinus,
faSearchPlus,
faServer,
@ -112,12 +117,15 @@ library.add(faAngleDoubleLeft);
library.add(faAngleDown);
library.add(faAngleRight);
library.add(faAngleUp);
library.add(faArrowLeft);
library.add(faArrowRight);
library.add(faAt);
library.add(faBook);
library.add(faBug);
library.add(faCalendar);
library.add(faCheck);
library.add(faChevronDown);
library.add(faChevronUp);
library.add(faCode);
library.add(faCodeBranch);
library.add(faCog);
@ -132,6 +140,7 @@ library.add(faEdit);
library.add(faEnvelope);
library.add(faEye);
library.add(faExclamationTriangle);
library.add(faExpand);
library.add(faExternalLinkAlt);
library.add(faExchangeAlt);
library.add(faFile);
@ -163,6 +172,7 @@ library.add(faQuestionCircle);
library.add(faRedo);
library.add(faRss);
library.add(faSave);
library.add(faSearch);
library.add(faSearchMinus);
library.add(faSearchPlus);
library.add(faServer);

View file

@ -1,24 +1,25 @@
import { getNewWorkflow } from '@/api/workflows';
import { getNewWorkflow, getWorkflowTemplate } from '@/api/workflows';
import { DUPLICATE_POSTFFIX, MAX_WORKFLOW_NAME_LENGTH, DEFAULT_NEW_WORKFLOW_NAME } from '@/constants';
import { ActionContext, Module } from 'vuex';
import {
IRootState,
IWorkflowsState,
IWorkflowTemplate,
} from '../Interface';
const module: Module<IWorkflowsState, IRootState> = {
namespaced: true,
state: {},
actions: {
setNewWorkflowName: async (context: ActionContext<IWorkflowsState, IRootState>): Promise<void> => {
setNewWorkflowName: async (context: ActionContext<IWorkflowsState, IRootState>, name?: string): Promise<void> => {
let newName = '';
try {
const newWorkflow = await getNewWorkflow(context.rootGetters.getRestApiContext);
const newWorkflow = await getNewWorkflow(context.rootGetters.getRestApiContext, name);
newName = newWorkflow.name;
}
catch (e) {
// in case of error, default to original name
newName = DEFAULT_NEW_WORKFLOW_NAME;
newName = name || DEFAULT_NEW_WORKFLOW_NAME;
}
context.commit('setWorkflowName', { newName }, { root: true });
@ -42,6 +43,9 @@ const module: Module<IWorkflowsState, IRootState> = {
return newName;
},
getWorkflowTemplate: async (context: ActionContext<IWorkflowsState, IRootState>, templateId: string): Promise<IWorkflowTemplate> => {
return await getWorkflowTemplate(templateId);
},
},
};

View file

@ -63,3 +63,23 @@ $--tag-text-color: #3d3f46;
$--tag-close-background-color: #717782;
$--tag-close-background-hover-color: #3d3f46;
// Node creator
$--node-creator-width: 385px;
$--node-creator-text-color: #555;
$--node-creator-select-background-color: #f2f4f8;
$--node-creator-background-color: #fff;
$--node-creator-search-background-color: #fff;
$--node-creator-border-color: #dbdfe7;
$--node-creator-item-hover-border-color: #8d939c;
$--node-creator-arrow-color: #8d939c;
$--node-creator-no-results-background-color: #f8f9fb;
$--node-creator-close-button-color: #fff;
$--node-creator-search-clear-background-color: #8d939c;
$--node-creator-search-clear-background-color-hover: #3d3f46;
$--node-creator-search-placeholder-color: #909399;
$--node-creator-subcategory-panel-header-bacground-color: #f2f4f8;
$--node-creator-description-color: #7d7d87;
// trigger icon
$--trigger-icon-border-color: #dcdfe6;
$--trigger-icon-background-color: #fff;

View file

@ -48,5 +48,14 @@ export default new Router({
path: '/',
redirect: '/workflow',
},
{
path: '/workflows/templates/:id',
name: 'WorkflowTemplate',
components: {
default: NodeView,
header: MainHeader,
sidebar: MainSidebar,
},
},
],
});

View file

@ -39,15 +39,18 @@
@closeNodeCreator="closeNodeCreator"
></node-creator>
<div :class="{ 'zoom-menu': true, expanded: !sidebarMenuCollapsed }">
<button @click="setZoom('in')" class="button-white" title="Zoom In">
<button @click="zoomToFit" class="button-white" title="Zoom to Fit">
<font-awesome-icon icon="expand"/>
</button>
<button @click="zoomIn()" class="button-white" title="Zoom In">
<font-awesome-icon icon="search-plus"/>
</button>
<button @click="setZoom('out')" class="button-white" title="Zoom Out">
<button @click="zoomOut()" class="button-white" title="Zoom Out">
<font-awesome-icon icon="search-minus"/>
</button>
<button
v-if="nodeViewScale !== 1"
@click="setZoom('reset')"
@click="resetZoom()"
class="button-white"
title="Reset Zoom"
>
@ -113,7 +116,7 @@ import {
} from 'jsplumb';
import { MessageBoxInputData } from 'element-ui/types/message-box';
import { jsPlumb, Endpoint, OnConnectionBindInfo } from 'jsplumb';
import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { NODE_NAME_PREFIX, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE } from '@/constants';
import { copyPaste } from '@/components/mixins/copyPaste';
import { externalHooks } from '@/components/mixins/externalHooks';
import { genericHelpers } from '@/components/mixins/genericHelpers';
@ -129,13 +132,14 @@ import { workflowRun } from '@/components/mixins/workflowRun';
import DataDisplay from '@/components/DataDisplay.vue';
import Modals from '@/components/Modals.vue';
import Node from '@/components/Node.vue';
import NodeCreator from '@/components/NodeCreator.vue';
import NodeCreator from '@/components/NodeCreator/NodeCreator.vue';
import NodeSettings from '@/components/NodeSettings.vue';
import RunData from '@/components/RunData.vue';
import { getLeftmostTopNode, getWorkflowCorners, scaleSmaller, scaleBigger, scaleReset } from './helpers';
import mixins from 'vue-typed-mixins';
import { v4 as uuidv4} from 'uuid';
import axios from 'axios';
import {
IConnection,
IConnections,
@ -144,7 +148,6 @@ import {
INodeConnections,
INodeIssues,
INodeTypeDescription,
IRunData,
NodeInputConnections,
NodeHelpers,
Workflow,
@ -155,19 +158,35 @@ import {
IExecutionResponse,
IExecutionsStopData,
IN8nUISettings,
IStartRunData,
IWorkflowDb,
IWorkflowData,
INodeUi,
IRunDataUi,
IUpdateInformation,
IWorkflowDataUpdate,
XYPositon,
IPushDataExecutionFinished,
ITag,
IWorkflowTemplate,
} from '../Interface';
import { mapGetters } from 'vuex';
const NODE_SIZE = 100;
const DEFAULT_START_POSITION_X = 250;
const DEFAULT_START_POSITION_Y = 300;
const HEADER_HEIGHT = 65;
const SIDEBAR_WIDTH = 65;
const DEFAULT_START_NODE = {
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [
DEFAULT_START_POSITION_X,
DEFAULT_START_POSITION_Y,
] as XYPositon,
parameters: {},
};
export default mixins(
copyPaste,
externalHooks,
@ -311,6 +330,7 @@ export default mixins(
nodeViewScale: 1,
ctrlKeyPressed: false,
stopExecutionInProgress: false,
blankRedirect: false,
};
},
beforeDestroy () {
@ -349,9 +369,67 @@ export default mixins(
this.$store.commit('setWorkflowExecutionData', data);
await this.addNodes(JSON.parse(JSON.stringify(data.workflowData.nodes)), JSON.parse(JSON.stringify(data.workflowData.connections)));
this.$nextTick(() => {
this.zoomToFit();
this.$store.commit('setStateDirty', false);
});
this.$externalHooks().run('execution.open', { workflowId: data.workflowData.id, workflowName: data.workflowData.name, executionId });
},
async openWorkflowTemplate (templateId: string) {
this.setLoadingText('Loading template');
this.resetWorkspace();
let data: IWorkflowTemplate | undefined;
try {
this.$externalHooks().run('template.requested', { templateId });
data = await this.$store.dispatch('workflows/getWorkflowTemplate', templateId);
if (!data) {
throw new Error(`Workflow template with id "${templateId}" could not be found!`);
}
data.workflow.nodes.forEach((node) => {
if (!this.$store.getters.nodeType(node.type)) {
const name = node.type.replace('n8n-nodes-base.', '');
throw new Error(`The ${name} node is not supported`);
}
});
} catch (error) {
this.$showError(error, `Couldn't import workflow`);
this.$router.push({ name: 'NodeViewNew' });
return;
}
const nodes = data.workflow.nodes;
const hasStartNode = !!nodes.find(node => node.type === START_NODE_TYPE);
const leftmostTop = getLeftmostTopNode(nodes);
const diffX = DEFAULT_START_POSITION_X - leftmostTop.position[0];
const diffY = DEFAULT_START_POSITION_Y - leftmostTop.position[1];
data.workflow.nodes.map((node) => {
node.position[0] += diffX + (hasStartNode? 0 : NODE_SIZE * 2);
node.position[1] += diffY;
});
if (!hasStartNode) {
data.workflow.nodes.push(DEFAULT_START_NODE);
}
this.blankRedirect = true;
this.$router.push({ name: 'NodeViewNew' });
await this.addNodes(data.workflow.nodes, data.workflow.connections);
await this.$store.dispatch('workflows/setNewWorkflowName', data.name);
this.$nextTick(() => {
this.zoomToFit();
this.$store.commit('setStateDirty', true);
});
this.$externalHooks().run('template.open', { templateId, templateName: data.name, workflow: data.workflow });
},
async openWorkflow (workflowId: string) {
this.resetWorkspace();
@ -381,6 +459,7 @@ export default mixins(
await this.addNodes(data.nodes, data.connections);
this.$store.commit('setStateDirty', false);
this.zoomToFit();
this.$externalHooks().run('workflow.open', { workflowId, workflowName: data.name });
@ -412,9 +491,9 @@ export default mixins(
//* Control + scroll zoom
if (e.ctrlKey) {
if (e.deltaY > 0) {
this.setZoom('out');
this.zoomOut();
} else {
this.setZoom('in');
this.zoomIn();
}
e.preventDefault();
@ -435,7 +514,9 @@ export default mixins(
// else which should ignore the default keybindings
for (let index = 0; index < path.length; index++) {
if (path[index].className && typeof path[index].className === 'string' && (
path[index].className.includes('el-message-box') || path[index].className.includes('ignore-key-press')
path[index].className.includes('el-message-box') ||
path[index].className.includes('el-select') ||
path[index].className.includes('ignore-key-press')
)) {
return;
}
@ -453,6 +534,7 @@ export default mixins(
this.callDebounced('deleteSelectedNodes', 500);
} else if (e.key === 'Escape') {
this.$externalHooks().run('dataDisplay.nodeEditingFinished');
this.createNodeActive = false;
this.$store.commit('setActiveNode', null);
} else if (e.key === 'Tab') {
@ -464,12 +546,14 @@ export default mixins(
if (lastSelectedNode !== null) {
this.callDebounced('renameNodePrompt', 1500, lastSelectedNode.name);
}
} else if (e.key === '+') {
this.callDebounced('setZoom', 300, 'in');
} else if (e.key === '-') {
this.callDebounced('setZoom', 300, 'out');
} else if ((e.key === '0') && (this.isCtrlKeyPressed(e) === true)) {
this.callDebounced('setZoom', 300, 'reset');
} else if ((e.key === '=' || e.key === '+') && !this.isCtrlKeyPressed(e)) {
this.zoomIn();
} else if ((e.key === '_' || e.key === '-') && !this.isCtrlKeyPressed(e)) {
this.zoomOut();
} else if ((e.key === '0') && !this.isCtrlKeyPressed(e)) {
this.resetZoom();
} else if ((e.key === '1') && !this.isCtrlKeyPressed(e)) {
this.zoomToFit();
} else if ((e.key === 'a') && (this.isCtrlKeyPressed(e) === true)) {
// Select all nodes
e.stopPropagation();
@ -703,18 +787,32 @@ export default mixins(
});
},
setZoom (zoom: string) {
if (zoom === 'in') {
this.nodeViewScale *= 1.25;
} else if (zoom === 'out') {
this.nodeViewScale /= 1.25;
} else {
this.nodeViewScale = 1;
}
resetZoom () {
const { scale, offset } = scaleReset({scale: this.nodeViewScale, offset: this.$store.getters.getNodeViewOffsetPosition});
const zoomLevel = this.nodeViewScale;
this.setZoomLevel(scale);
this.$store.commit('setNodeViewOffsetPosition', {newOffset: offset});
},
zoomIn() {
const { scale, offset: [xOffset, yOffset] } = scaleBigger({scale: this.nodeViewScale, offset: this.$store.getters.getNodeViewOffsetPosition});
this.setZoomLevel(scale);
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [xOffset, yOffset]});
},
zoomOut() {
const { scale, offset: [xOffset, yOffset] } = scaleSmaller({scale: this.nodeViewScale, offset: this.$store.getters.getNodeViewOffsetPosition});
this.setZoomLevel(scale);
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [xOffset, yOffset]});
},
setZoomLevel (zoomLevel: number) {
this.nodeViewScale = zoomLevel; // important for background
const element = this.instance.getContainer() as HTMLElement;
// https://docs.jsplumbtoolkit.com/community/current/articles/zooming.html
const prependProperties = ['webkit', 'moz', 'ms', 'o'];
const scaleString = 'scale(' + zoomLevel + ')';
@ -728,6 +826,36 @@ export default mixins(
this.instance.setZoom(zoomLevel);
},
zoomToFit () {
const nodes = this.$store.getters.allNodes as INodeUi[];
if (nodes.length === 0) { // some unknown workflow executions
return;
}
const {minX, minY, maxX, maxY} = getWorkflowCorners(nodes);
const PADDING = NODE_SIZE * 4;
const editorWidth = window.innerWidth;
const diffX = maxX - minX + SIDEBAR_WIDTH + PADDING;
const scaleX = editorWidth / diffX;
const editorHeight = window.innerHeight;
const diffY = maxY - minY + HEADER_HEIGHT + PADDING;
const scaleY = editorHeight / diffY;
const zoomLevel = Math.min(scaleX, scaleY, 1);
let xOffset = (minX * -1) * zoomLevel + SIDEBAR_WIDTH; // find top right corner
xOffset += (editorWidth - SIDEBAR_WIDTH - (maxX - minX + NODE_SIZE) * zoomLevel) / 2; // add padding to center workflow
let yOffset = (minY * -1) * zoomLevel + HEADER_HEIGHT; // find top right corner
yOffset += (editorHeight - HEADER_HEIGHT - (maxY - minY + NODE_SIZE * 2) * zoomLevel) / 2; // add padding to center workflow
this.setZoomLevel(zoomLevel);
this.$store.commit('setNodeViewOffsetPosition', {newOffset: [xOffset, yOffset]});
},
async stopExecution () {
const executionId = this.$store.getters.activeExecutionId;
if (executionId === null) {
@ -1405,23 +1533,10 @@ export default mixins(
await this.$store.dispatch('workflows/setNewWorkflowName');
this.$store.commit('setStateDirty', false);
// Create start node
const defaultNodes = [
{
name: 'Start',
type: 'n8n-nodes-base.start',
typeVersion: 1,
position: [
250,
300,
] as XYPositon,
parameters: {},
},
];
await this.addNodes(defaultNodes);
await this.addNodes([DEFAULT_START_NODE]);
this.$store.commit('setStateDirty', false);
this.setZoomLevel(1);
},
async initView (): Promise<void> {
if (this.$route.params.action === 'workflowSave') {
@ -1431,7 +1546,14 @@ export default mixins(
return Promise.resolve();
}
if (this.$route.name === 'ExecutionById') {
if (this.blankRedirect) {
this.blankRedirect = false;
}
else if (this.$route.name === 'WorkflowTemplate') {
const templateId = this.$route.params.id;
await this.openWorkflowTemplate(templateId);
}
else if (this.$route.name === 'ExecutionById') {
// Load an execution
const executionId = this.$route.params.id;
await this.openExecution(executionId);

View file

@ -0,0 +1,85 @@
import { INodeUi, IZoomConfig } from "@/Interface";
interface ICorners {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
return nodes.reduce((leftmostTop, node) => {
if (node.position[0] > leftmostTop.position[0] || node.position[1] > leftmostTop.position[1]) {
return leftmostTop;
}
return node;
});
};
export const getWorkflowCorners = (nodes: INodeUi[]): ICorners => {
return nodes.reduce((accu: ICorners, node: INodeUi) => {
if (node.position[0] < accu.minX) {
accu.minX = node.position[0];
}
if (node.position[1] < accu.minY) {
accu.minY = node.position[1];
}
if (node.position[0] > accu.maxX) {
accu.maxX = node.position[0];
}
if (node.position[1] > accu.maxY) {
accu.maxY = node.position[1];
}
return accu;
}, {
minX: nodes[0].position[0],
minY: nodes[0].position[1],
maxX: nodes[0].position[0],
maxY: nodes[0].position[1],
});
};
export const scaleSmaller = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
scale /= 1.25;
xOffset /= 1.25;
yOffset /= 1.25;
xOffset += window.innerWidth / 10;
yOffset += window.innerHeight / 10;
return {
scale,
offset: [xOffset, yOffset],
};
};
export const scaleBigger = ({scale, offset: [xOffset, yOffset]}: IZoomConfig): IZoomConfig => {
scale *= 1.25;
xOffset -= window.innerWidth / 10;
yOffset -= window.innerHeight / 10;
xOffset *= 1.25;
yOffset *= 1.25;
return {
scale,
offset: [xOffset, yOffset],
};
};
export const scaleReset = (config: IZoomConfig): IZoomConfig => {
if (config.scale > 1) { // zoomed in
while (config.scale > 1) {
config = scaleSmaller(config);
}
}
else {
while (config.scale < 1) {
config = scaleBigger(config);
}
}
config.scale = 1;
return config;
};

View file

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "0.13.0",
"version": "0.15.0",
"description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -59,8 +59,8 @@
"change-case": "^4.1.1",
"copyfiles": "^2.1.1",
"inquirer": "^7.0.1",
"n8n-core": "~0.71.0",
"n8n-workflow": "~0.58.0",
"n8n-core": "~0.75.0",
"n8n-workflow": "~0.62.0",
"oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0",
"request": "^2.88.2",

View file

@ -118,7 +118,12 @@ export async function buildFiles (options?: IBuildOptions): Promise<string> {
}
return new Promise((resolve, reject) => {
copyfiles([join(process.cwd(), './*.png'), outputDirectory], { up: true }, () => resolve(outputDirectory));
['*.png', '*.node.json'].forEach(filenamePattern => {
copyfiles(
[join(process.cwd(), `./${filenamePattern}`), outputDirectory],
{ up: true },
() => resolve(outputDirectory));
});
buildProcess.on('exit', code => {
// Remove the tmp tsconfig file
tsconfigData.cleanup();

View file

@ -0,0 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class ActionNetworkApi implements ICredentialType {
name = 'actionNetworkApi';
displayName = 'Action Network API';
properties = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
default: '',
},
];
}

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -8,17 +8,17 @@ export class ActiveCampaignApi implements ICredentialType {
name = 'activeCampaignApi';
displayName = 'ActiveCampaign API';
documentationUrl = 'activeCampaign';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API URL',
name: 'apiUrl',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,23 +1,23 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class AcuitySchedulingApi implements ICredentialType {
name = 'acuitySchedulingApi';
displayName = 'Acuity Scheduling API';
documentationUrl = 'acuityScheduling';
properties = [
properties: INodeProperties[] = [
{
displayName: 'User ID',
name: 'userId',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -11,38 +11,38 @@ export class AcuitySchedulingOAuth2Api implements ICredentialType {
];
displayName = 'AcuityScheduling OAuth2 API';
documentationUrl = 'acuityScheduling';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://acuityscheduling.com/oauth2/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://acuityscheduling.com/oauth2/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'api-v1',
required: true,
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'body',
},
];

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class AffinityApi implements ICredentialType {
name = 'affinityApi';
displayName = 'Affinity API';
documentationUrl = 'affinity';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,29 +1,29 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class AgileCrmApi implements ICredentialType {
name = 'agileCrmApi';
displayName = 'AgileCRM API';
documentationUrl = 'agileCrm';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Email',
name: 'email',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Subdomain',
name: 'subdomain',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
placeholder: 'example',
description: 'If the domain is https://example.agilecrm.com "example" would have to be entered.',

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -8,11 +8,11 @@ export class AirtableApi implements ICredentialType {
name = 'airtableApi';
displayName = 'Airtable API';
documentationUrl = 'airtable';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -8,29 +8,29 @@ export class Amqp implements ICredentialType {
name = 'amqp';
displayName = 'AMQP';
documentationUrl = 'amqp';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Hostname',
name: 'hostname',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Port',
name: 'port',
type: 'number' as NodePropertyTypes,
type: 'number',
default: 5672,
},
{
displayName: 'User',
name: 'username',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
type: 'string',
typeOptions: {
password: true,
},
@ -39,7 +39,7 @@ export class Amqp implements ICredentialType {
{
displayName: 'Transport Type',
name: 'transportType',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
description: 'Optional Transport Type to use.',
},

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class ApiTemplateIoApi implements ICredentialType {
name = 'apiTemplateIoApi';
displayName = 'APITemplate.io API';
documentationUrl = 'apiTemplateIo';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class AsanaApi implements ICredentialType {
name = 'asanaApi';
displayName = 'Asana API';
documentationUrl = 'asana';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class AsanaOAuth2Api implements ICredentialType {
@ -10,37 +10,37 @@ export class AsanaOAuth2Api implements ICredentialType {
];
displayName = 'Asana OAuth2 API';
documentationUrl = 'asana';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://app.asana.com/-/oauth_authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://app.asana.com/-/oauth_token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: '',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'body',
description: 'Resource to consume.',
},

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class AutomizyApi implements ICredentialType {
name = 'automizyApi';
displayName = 'Automizy API';
documentationUrl = 'automizy';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Token',
name: 'apiToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class AutopilotApi implements ICredentialType {
name = 'autopilotApi';
displayName = 'Autopilot API';
documentationUrl = 'autopilot';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -8,23 +8,23 @@ export class Aws implements ICredentialType {
name = 'aws';
displayName = 'AWS';
documentationUrl = 'aws';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Region',
name: 'region',
type: 'string' as NodePropertyTypes,
type: 'string',
default: 'us-east-1',
},
{
displayName: 'Access Key Id',
name: 'accessKeyId',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Secret Access Key',
name: 'secretAccessKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
typeOptions: {
password: true,
@ -33,14 +33,14 @@ export class Aws implements ICredentialType {
{
displayName: 'Custom Endpoints',
name: 'customEndpoints',
type: 'boolean' as NodePropertyTypes,
type: 'boolean',
default: false,
},
{
displayName: 'Rekognition Endpoint',
name: 'rekognitionEndpoint',
description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and Rekognition using a VPC endpoint. Leave blank to use the default endpoint.',
type: 'string' as NodePropertyTypes,
type: 'string',
displayOptions: {
show: {
customEndpoints: [
@ -55,7 +55,7 @@ export class Aws implements ICredentialType {
displayName: 'Lambda Endpoint',
name: 'lambdaEndpoint',
description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and Lambda using a VPC endpoint. Leave blank to use the default endpoint.',
type: 'string' as NodePropertyTypes,
type: 'string',
displayOptions: {
show: {
customEndpoints: [
@ -70,7 +70,7 @@ export class Aws implements ICredentialType {
displayName: 'SNS Endpoint',
name: 'snsEndpoint',
description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and SNS using a VPC endpoint. Leave blank to use the default endpoint.',
type: 'string' as NodePropertyTypes,
type: 'string',
displayOptions: {
show: {
customEndpoints: [
@ -85,7 +85,7 @@ export class Aws implements ICredentialType {
displayName: 'SES Endpoint',
name: 'sesEndpoint',
description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and SES using a VPC endpoint. Leave blank to use the default endpoint.',
type: 'string' as NodePropertyTypes,
type: 'string',
displayOptions: {
show: {
customEndpoints: [
@ -100,7 +100,7 @@ export class Aws implements ICredentialType {
displayName: 'SQS Endpoint',
name: 'sqsEndpoint',
description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and SQS using a VPC endpoint. Leave blank to use the default endpoint.',
type: 'string' as NodePropertyTypes,
type: 'string',
displayOptions: {
show: {
customEndpoints: [
@ -115,7 +115,7 @@ export class Aws implements ICredentialType {
displayName: 'S3 Endpoint',
name: 's3Endpoint',
description: 'If you use Amazon VPC to host n8n, you can establish a connection between your VPC and S3 using a VPC endpoint. Leave blank to use the default endpoint.',
type: 'string' as NodePropertyTypes,
type: 'string',
displayOptions: {
show: {
customEndpoints: [

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class BannerbearApi implements ICredentialType {
name = 'bannerbearApi';
displayName = 'Bannerbear API';
documentationUrl = 'bannerbear';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Project API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,22 +1,23 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class BeeminderApi implements ICredentialType {
name = 'beeminderApi';
displayName = 'Beeminder API';
properties = [
documentationUrl = 'beeminder';
properties: INodeProperties[] = [
{
displayName: 'User',
name: 'user',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Auth Token',
name: 'authToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,23 +1,23 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class BitbucketApi implements ICredentialType {
name = 'bitbucketApi';
displayName = 'Bitbucket API';
documentationUrl = 'bitbucket';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Username',
name: 'username',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'App Password',
name: 'appPassword',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class BitlyApi implements ICredentialType {
name = 'bitlyApi';
displayName = 'Bitly API';
documentationUrl = 'bitly';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -11,32 +11,32 @@ export class BitlyOAuth2Api implements ICredentialType {
extends = [
'oAuth2Api',
];
properties = [
properties: INodeProperties[] = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://bitly.com/oauth/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://api-ssl.bitly.com/oauth/access_token',
required: true,
},
{
displayName: 'Client ID',
name: 'clientId',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
required: true,
},
{
displayName: 'Client Secret',
name: 'clientSecret',
type: 'string' as NodePropertyTypes,
type: 'string',
typeOptions: {
password: true,
},
@ -46,13 +46,13 @@ export class BitlyOAuth2Api implements ICredentialType {
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: '',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: '',
description: 'For some services additional query parameters have to be set which can be defined here.',
placeholder: '',
@ -60,7 +60,7 @@ export class BitlyOAuth2Api implements ICredentialType {
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'body',
description: 'Resource to consume.',
},

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
// https://bitwarden.com/help/article/public-api/#authentication
@ -9,23 +9,23 @@ export class BitwardenApi implements ICredentialType {
name = 'bitwardenApi';
displayName = 'Bitwarden API';
documentationUrl = 'bitwarden';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Client ID',
name: 'clientId',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Client Secret',
name: 'clientSecret',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Environment',
name: 'environment',
type: 'options' as NodePropertyTypes,
type: 'options',
default: 'cloudHosted',
options: [
{
@ -41,7 +41,7 @@ export class BitwardenApi implements ICredentialType {
{
displayName: 'Self-hosted domain',
name: 'domain',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
placeholder: 'https://www.mydomain.com',
displayOptions: {

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class BoxOAuth2Api implements ICredentialType {
@ -10,37 +10,37 @@ export class BoxOAuth2Api implements ICredentialType {
];
displayName = 'Box OAuth2 API';
documentationUrl = 'box';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://account.box.com/api/oauth2/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://api.box.com/oauth2/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: '',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'body',
},
];

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class BrandfetchApi implements ICredentialType {
name = 'brandfetchApi';
displayName = 'Brandfetch API';
documentationUrl = 'brandfetch';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,29 +1,29 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class BubbleApi implements ICredentialType {
name = 'bubbleApi';
displayName = 'Bubble API';
documentationUrl = 'bubble';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Token',
name: 'apiToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'App Name',
name: 'appName',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Environment',
name: 'environment',
type: 'options' as NodePropertyTypes,
type: 'options',
default: 'live',
options: [
{
@ -39,7 +39,7 @@ export class BubbleApi implements ICredentialType {
{
displayName: 'Hosting',
name: 'hosting',
type: 'options' as NodePropertyTypes,
type: 'options',
default: 'bubbleHosted',
options: [
{
@ -55,7 +55,7 @@ export class BubbleApi implements ICredentialType {
{
displayName: 'Domain',
name: 'domain',
type: 'string' as NodePropertyTypes,
type: 'string',
placeholder: 'mydomain.com',
default: '',
displayOptions: {

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class CalendlyApi implements ICredentialType {
name = 'calendlyApi';
displayName = 'Calendly API';
documentationUrl = 'calendly';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -8,17 +8,17 @@ export class ChargebeeApi implements ICredentialType {
name = 'chargebeeApi';
displayName = 'Chargebee API';
documentationUrl = 'chargebee';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Account Name',
name: 'accountName',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Api Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class CircleCiApi implements ICredentialType {
name = 'circleCiApi';
displayName = 'CircleCI API';
documentationUrl = 'circleCi';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Personal API Token',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class ClearbitApi implements ICredentialType {
name = 'clearbitApi';
displayName = 'Clearbit API';
documentationUrl = 'clearbit';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class ClickUpApi implements ICredentialType {
name = 'clickUpApi';
displayName = 'ClickUp API';
documentationUrl = 'clickUp';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class ClickUpOAuth2Api implements ICredentialType {
@ -10,37 +10,37 @@ export class ClickUpOAuth2Api implements ICredentialType {
];
displayName = 'ClickUp OAuth2 API';
documentationUrl = 'clickUp';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://app.clickup.com/api',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://api.clickup.com/api/v2/oauth/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: '',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'body',
description: 'Resource to consume.',
},

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -8,14 +8,14 @@ export class ClockifyApi implements ICredentialType {
name = 'clockifyApi';
displayName = 'Clockify API';
documentationUrl = 'clockify';
properties = [
properties: INodeProperties[] = [
// The credentials to get from user and save encrypted.
// Properties can be defined exactly in the same way
// as node properties.
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,24 +1,24 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class CockpitApi implements ICredentialType {
name = 'cockpitApi';
displayName = 'Cockpit API';
documentationUrl = 'cockpit';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Cockpit URL',
name: 'url',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
placeholder: 'https://example.com',
},
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class CodaApi implements ICredentialType {
name = 'codaApi';
displayName = 'Coda API';
documentationUrl = 'coda';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
//https://www.contentful.com/developers/docs/references/authentication/
@ -8,11 +8,11 @@ export class ContentfulApi implements ICredentialType {
name = 'contentfulApi';
displayName = 'Contenful API';
documentationUrl = 'contentful';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Space ID',
name: 'spaceId',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
required: true,
description: 'The id for the Contentful space.',
@ -20,14 +20,14 @@ export class ContentfulApi implements ICredentialType {
{
displayName: 'Content Delivery API Access token',
name: 'ContentDeliveryaccessToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
description: 'Access token that has access to the space. Can be left empty if only Delivery API should be used.',
},
{
displayName: 'Content Preview API Access token',
name: 'ContentPreviewaccessToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
description: 'Access token that has access to the space. Can be left empty if only Preview API should be used.',
},

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -8,11 +8,11 @@ export class ConvertKitApi implements ICredentialType {
name = 'convertKitApi';
displayName = 'ConvertKit API';
documentationUrl = 'convertKit';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Secret',
name: 'apiSecret',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
typeOptions: {
password: true,

View file

@ -1,25 +1,25 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class CopperApi implements ICredentialType {
name = 'copperApi';
displayName = 'Copper API';
documentationUrl = 'copper';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
required: true,
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Email',
name: 'email',
required: true,
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -8,17 +8,17 @@ export class CortexApi implements ICredentialType {
name = 'cortexApi';
displayName = 'Cortex API';
documentationUrl = 'cortex';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'cortexApiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Cortex Instance',
name: 'host',
type: 'string' as NodePropertyTypes,
type: 'string',
description: 'The URL of the Cortex instance',
default: '',
placeholder: 'https://localhost:9001',

View file

@ -1,32 +1,32 @@
import { ICredentialType, NodePropertyTypes } from 'n8n-workflow';
import { ICredentialType, INodeProperties } from 'n8n-workflow';
export class CrateDb implements ICredentialType {
name = 'crateDb';
displayName = 'CrateDB';
documentationUrl = 'crateDb';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Host',
name: 'host',
type: 'string' as NodePropertyTypes,
type: 'string',
default: 'localhost',
},
{
displayName: 'Database',
name: 'database',
type: 'string' as NodePropertyTypes,
type: 'string',
default: 'doc',
},
{
displayName: 'User',
name: 'user',
type: 'string' as NodePropertyTypes,
type: 'string',
default: 'crate',
},
{
displayName: 'Password',
name: 'password',
type: 'string' as NodePropertyTypes,
type: 'string',
typeOptions: {
password: true,
},
@ -35,7 +35,7 @@ export class CrateDb implements ICredentialType {
{
displayName: 'SSL',
name: 'ssl',
type: 'options' as NodePropertyTypes,
type: 'options',
options: [
{
name: 'disable',
@ -63,7 +63,7 @@ export class CrateDb implements ICredentialType {
{
displayName: 'Port',
name: 'port',
type: 'number' as NodePropertyTypes,
type: 'number',
default: 5432,
},
];

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -8,11 +8,11 @@ export class CustomerIoApi implements ICredentialType {
name = 'customerIoApi';
displayName = 'Customer.io API';
documentationUrl = 'customerIo';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Tracking API Key',
name: 'trackingApiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
description: 'Required for tracking API.',
required: true,
@ -20,14 +20,14 @@ export class CustomerIoApi implements ICredentialType {
{
displayName: 'Tracking Site ID',
name: 'trackingSiteId',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
description: 'Required for tracking API.',
},
{
displayName: 'App API Key',
name: 'appApiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
description: 'Required for App API.',
},

View file

@ -1,20 +1,20 @@
import { ICredentialType, NodePropertyTypes } from 'n8n-workflow';
import { ICredentialType, INodeProperties } from 'n8n-workflow';
export class DeepLApi implements ICredentialType {
name = 'deepLApi';
displayName = 'DeepL API';
documentationUrl = 'deepL';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'API Plan',
name: 'apiPlan',
type: 'options' as NodePropertyTypes,
type: 'options',
options: [
{
name: 'Pro Plan',

View file

@ -1,23 +1,23 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class DemioApi implements ICredentialType {
name = 'demioApi';
displayName = 'Demio API';
documentationUrl = 'demio';
properties = [
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'API Secret',
name: 'apiSecret',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,32 +1,32 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class DiscourseApi implements ICredentialType {
name = 'discourseApi';
displayName = 'Discourse API';
documentationUrl = 'discourse';
properties = [
properties: INodeProperties[] = [
{
displayName: 'URL',
name: 'url',
required: true,
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'API Key',
name: 'apiKey',
required: true,
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
{
displayName: 'Username',
name: 'username',
required: true,
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
},
];

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class DisqusApi implements ICredentialType {
name = 'disqusApi';
displayName = 'Disqus API';
documentationUrl = 'disqus';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
description: 'Visit your account details page, and grab the Access Token. See <a href="https://disqus.com/api/docs/auth/">Disqus auth</a>.',
},

View file

@ -1,17 +1,17 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
export class DriftApi implements ICredentialType {
name = 'driftApi';
displayName = 'Drift API';
documentationUrl = 'drift';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Personal Access Token',
name: 'accessToken',
type: 'string' as NodePropertyTypes,
type: 'string',
default: '',
description: 'Visit your account details page, and grab the Access Token. See <a href="https://devdocs.drift.com/docs/quick-start">Drift auth</a>.',
},

View file

@ -1,6 +1,6 @@
import {
ICredentialType,
NodePropertyTypes,
INodeProperties,
} from 'n8n-workflow';
@ -11,37 +11,37 @@ export class DriftOAuth2Api implements ICredentialType {
];
displayName = 'Drift OAuth2 API';
documentationUrl = 'drift';
properties = [
properties: INodeProperties[] = [
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://dev.drift.com/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'https://driftapi.com/oauth2/token',
required: true,
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: '',
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
type: 'hidden',
default: 'body',
},
];

Some files were not shown because too many files have changed in this diff Show more