mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
Merge branch 'n8n-io:master' into Add-schema-registry-into-kafka
This commit is contained in:
commit
8a22cc7114
|
@ -14,3 +14,7 @@ indent_size = 2
|
|||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.yml]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
|
40
.github/workflows/docker-images-rpi.yml
vendored
40
.github/workflows/docker-images-rpi.yml
vendored
|
@ -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
|
54
.github/workflows/docker-images.yml
vendored
54
.github/workflows/docker-images.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 .
|
||||
```
|
||||
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -673,10 +673,13 @@ class App {
|
|||
await WorkflowHelpers.validateWorkflow(updateData);
|
||||
await Db.collections.Workflow!.update(id, updateData).catch(WorkflowHelpers.throwDuplicateEntryError);
|
||||
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
await TagHelpers.removeRelations(req.params.id, tablePrefix);
|
||||
if (tags?.length) {
|
||||
await TagHelpers.createRelations(req.params.id, tags, tablePrefix);
|
||||
if (tags) {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
await TagHelpers.removeRelations(req.params.id, tablePrefix);
|
||||
|
||||
if (tags.length) {
|
||||
await TagHelpers.createRelations(req.params.id, tags, tablePrefix);
|
||||
}
|
||||
}
|
||||
|
||||
// We sadly get nothing back from "update". Neither if it updated a record
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
332
packages/editor-ui/src/components/NodeCreator/MainPanel.vue
Normal file
332
packages/editor-ui/src/components/NodeCreator/MainPanel.vue
Normal 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>
|
126
packages/editor-ui/src/components/NodeCreator/NoResults.vue
Normal file
126
packages/editor-ui/src/components/NodeCreator/NoResults.vue
Normal 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">
|
||||
Don’t 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>
|
||||
<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>
|
|
@ -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>
|
101
packages/editor-ui/src/components/NodeCreator/NodeCreator.vue
Normal file
101
packages/editor-ui/src/components/NodeCreator/NodeCreator.vue
Normal 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>
|
86
packages/editor-ui/src/components/NodeCreator/NodeItem.vue
Normal file
86
packages/editor-ui/src/components/NodeCreator/NodeItem.vue
Normal 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>
|
124
packages/editor-ui/src/components/NodeCreator/SearchBar.vue
Normal file
124
packages/editor-ui/src/components/NodeCreator/SearchBar.vue
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
176
packages/editor-ui/src/components/NodeCreator/helpers.ts
Normal file
176
packages/editor-ui/src/components/NodeCreator/helpers.ts
Normal 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);
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
42
packages/editor-ui/src/components/TriggerIcon.vue
Normal file
42
packages/editor-ui/src/components/TriggerIcon.vue
Normal 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>
|
|
@ -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();
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
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) {
|
||||
$showMessage(messageData: ElNotificationOptions) {
|
||||
messageData.dangerouslyUseHTMLString = true;
|
||||
if (messageData.position === undefined) {
|
||||
messageData.position = 'bottom-right';
|
||||
|
@ -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>
|
||||
`;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -48,5 +48,14 @@ export default new Router({
|
|||
path: '/',
|
||||
redirect: '/workflow',
|
||||
},
|
||||
{
|
||||
path: '/workflows/templates/:id',
|
||||
name: 'WorkflowTemplate',
|
||||
components: {
|
||||
default: NodeView,
|
||||
header: MainHeader,
|
||||
sidebar: MainSidebar,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
85
packages/editor-ui/src/views/helpers.ts
Normal file
85
packages/editor-ui/src/views/helpers.ts
Normal 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;
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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.',
|
||||
},
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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.',
|
||||
},
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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.',
|
||||
},
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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.',
|
||||
},
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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.',
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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.',
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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>.',
|
||||
},
|
||||
|
|
|
@ -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>.',
|
||||
},
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue