mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge branch 'master' of github.com:n8n-io/n8n into ask-assistant
This commit is contained in:
commit
fe219c50fa
44
.github/scripts/ensure-provenance-fields.mjs
vendored
Normal file
44
.github/scripts/ensure-provenance-fields.mjs
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
import { writeFile, readFile, copyFile } from 'fs/promises';
|
||||
import { resolve, dirname } from 'path';
|
||||
import child_process from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const exec = promisify(child_process.exec);
|
||||
|
||||
const commonFiles = ['LICENSE.md', 'LICENSE_EE.md'];
|
||||
|
||||
const baseDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const packages = JSON.parse((await exec('pnpm ls -r --only-projects --json')).stdout);
|
||||
|
||||
for (let { name, path, version, private: isPrivate } of packages) {
|
||||
if (isPrivate) continue;
|
||||
|
||||
const packageFile = resolve(path, 'package.json');
|
||||
const packageJson = {
|
||||
...JSON.parse(await readFile(packageFile, 'utf-8')),
|
||||
// Add these fields to all published package.json files to ensure provenance checks pass
|
||||
license: 'SEE LICENSE IN LICENSE.md',
|
||||
homepage: 'https://n8n.io',
|
||||
author: {
|
||||
name: 'Jan Oberhauser',
|
||||
email: 'jan@n8n.io',
|
||||
},
|
||||
repository: {
|
||||
type: 'git',
|
||||
url: 'git+https://github.com/n8n-io/n8n.git',
|
||||
},
|
||||
};
|
||||
|
||||
// Copy over LICENSE.md and LICENSE_EE.md into every published package, and ensure they get included in the published package
|
||||
await Promise.all(
|
||||
commonFiles.map(async (file) => {
|
||||
await copyFile(resolve(baseDir, file), resolve(path, file));
|
||||
if (packageJson.files && !packageJson.files.includes(file)) {
|
||||
packageJson.files.push(file);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
await writeFile(packageFile, JSON.stringify(packageJson, null, 2) + '\n');
|
||||
}
|
|
@ -1,12 +1,15 @@
|
|||
const { writeFileSync } = require('fs');
|
||||
const { resolve } = require('path');
|
||||
const baseDir = resolve(__dirname, '..');
|
||||
const baseDir = resolve(__dirname, '../..');
|
||||
|
||||
const trimPackageJson = (packageName) => {
|
||||
const filePath = resolve(baseDir, 'packages', packageName, 'package.json');
|
||||
const { scripts, peerDependencies, devDependencies, dependencies, ...packageJson } = require(
|
||||
filePath,
|
||||
);
|
||||
if (packageName === '@n8n/chat') {
|
||||
packageJson.dependencies = dependencies;
|
||||
}
|
||||
writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
|
||||
};
|
||||
|
3
.github/workflows/release-publish.yml
vendored
3
.github/workflows/release-publish.yml
vendored
|
@ -45,7 +45,8 @@ jobs:
|
|||
- name: Publish to NPM
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
node scripts/trim-fe-packageJson.js
|
||||
node .github/scripts/trim-fe-packageJson.js
|
||||
node .github/scripts/ensure-provenance-fields.mjs
|
||||
sed -i "s/default: 'dev'/default: 'stable'/g" packages/cli/dist/config/schema.js
|
||||
pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks
|
||||
npm dist-tag rm n8n rc
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
"name": "n8n-monorepo",
|
||||
"version": "1.49.0",
|
||||
"private": true,
|
||||
"homepage": "https://n8n.io",
|
||||
"engines": {
|
||||
"node": ">=18.10",
|
||||
"pnpm": ">=9.1"
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
# License
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- Content of branches other than the main branch (i.e. "master") are not licensed.
|
||||
- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License.
|
||||
To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License
|
||||
specifically allowing you access to such source code files and as defined in "LICENSE_EE.md".
|
||||
- All third party components incorporated into the n8n Software are licensed under the original license
|
||||
provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
|
||||
License" as defined below.
|
||||
|
||||
## Sustainable Use License
|
||||
|
||||
Version 1.0
|
||||
|
||||
### Acceptance
|
||||
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
### Copyright License
|
||||
|
||||
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
||||
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
||||
to the limitations below.
|
||||
|
||||
### Limitations
|
||||
|
||||
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
||||
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
||||
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
||||
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
||||
|
||||
### Patents
|
||||
|
||||
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
||||
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
||||
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
||||
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
||||
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
||||
license ends immediately for work on behalf of your company.
|
||||
|
||||
### Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
||||
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
||||
stating that you have modified the software.
|
||||
|
||||
### No Other Rights
|
||||
|
||||
These terms do not imply any licenses other than those expressly granted in these terms.
|
||||
|
||||
### Termination
|
||||
|
||||
If you use the software in violation of these terms, such use is not licensed, and your license will
|
||||
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
||||
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
||||
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
||||
terms will cause your license to terminate automatically and permanently.
|
||||
|
||||
### No Liability
|
||||
|
||||
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
||||
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
||||
any kind of legal claim.
|
||||
|
||||
### Definitions
|
||||
|
||||
The “licensor” is the entity offering these terms.
|
||||
|
||||
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
||||
|
||||
“You” refers to the individual or entity agreeing to these terms.
|
||||
|
||||
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
||||
all organizations that have control over, are under the control of, or are under common control with that
|
||||
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
||||
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
“Your license” is the license granted to you for the software under these terms.
|
||||
|
||||
“Use” means anything you do with the software requiring your license.
|
||||
|
||||
“Trademark” means trademarks, service marks, and similar rights.
|
|
@ -14,8 +14,7 @@
|
|||
"lintfix": "eslint . --ext .js,.ts,.vue --fix",
|
||||
"format": "prettier --write src/",
|
||||
"storybook": "storybook dev -p 6006 --no-open",
|
||||
"build:storybook": "storybook build",
|
||||
"release": "pnpm run build:full && cd dist && pnpm publish"
|
||||
"build:storybook": "storybook build"
|
||||
},
|
||||
"main": "./dist/chat.umd.js",
|
||||
"module": "./dist/chat.es.js",
|
||||
|
@ -52,13 +51,6 @@
|
|||
},
|
||||
"files": [
|
||||
"README.md",
|
||||
"LICENSE.md",
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/client-oauth2",
|
||||
"version": "0.18.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/imap",
|
||||
"version": "0.5.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
# License
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- Content of branches other than the main branch (i.e. "master") are not licensed.
|
||||
- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License.
|
||||
To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License
|
||||
specifically allowing you access to such source code files and as defined in "LICENSE_EE.md".
|
||||
- All third party components incorporated into the n8n Software are licensed under the original license
|
||||
provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
|
||||
License" as defined below.
|
||||
|
||||
## Sustainable Use License
|
||||
|
||||
Version 1.0
|
||||
|
||||
### Acceptance
|
||||
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
### Copyright License
|
||||
|
||||
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
||||
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
||||
to the limitations below.
|
||||
|
||||
### Limitations
|
||||
|
||||
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
||||
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
||||
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
||||
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
||||
|
||||
### Patents
|
||||
|
||||
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
||||
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
||||
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
||||
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
||||
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
||||
license ends immediately for work on behalf of your company.
|
||||
|
||||
### Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
||||
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
||||
stating that you have modified the software.
|
||||
|
||||
### No Other Rights
|
||||
|
||||
These terms do not imply any licenses other than those expressly granted in these terms.
|
||||
|
||||
### Termination
|
||||
|
||||
If you use the software in violation of these terms, such use is not licensed, and your license will
|
||||
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
||||
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
||||
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
||||
terms will cause your license to terminate automatically and permanently.
|
||||
|
||||
### No Liability
|
||||
|
||||
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
||||
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
||||
any kind of legal claim.
|
||||
|
||||
### Definitions
|
||||
|
||||
The “licensor” is the entity offering these terms.
|
||||
|
||||
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
||||
|
||||
“You” refers to the individual or entity agreeing to these terms.
|
||||
|
||||
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
||||
all organizations that have control over, are under the control of, or are under common control with that
|
||||
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
||||
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
“Your license” is the license granted to you for the software under these terms.
|
||||
|
||||
“Use” means anything you do with the software requiring your license.
|
||||
|
||||
“Trademark” means trademarks, service marks, and similar rights.
|
|
@ -2,12 +2,6 @@
|
|||
"name": "@n8n/n8n-nodes-langchain",
|
||||
"version": "1.49.0",
|
||||
"description": "",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{
|
||||
"name": "@n8n/permissions",
|
||||
"version": "0.10.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
# License
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- Content of branches other than the main branch (i.e. "master") are not licensed.
|
||||
- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License.
|
||||
To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License
|
||||
specifically allowing you access to such source code files and as defined in "LICENSE_EE.md".
|
||||
- All third party components incorporated into the n8n Software are licensed under the original license
|
||||
provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
|
||||
License" as defined below.
|
||||
|
||||
## Sustainable Use License
|
||||
|
||||
Version 1.0
|
||||
|
||||
### Acceptance
|
||||
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
### Copyright License
|
||||
|
||||
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
||||
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
||||
to the limitations below.
|
||||
|
||||
### Limitations
|
||||
|
||||
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
||||
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
||||
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
||||
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
||||
|
||||
### Patents
|
||||
|
||||
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
||||
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
||||
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
||||
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
||||
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
||||
license ends immediately for work on behalf of your company.
|
||||
|
||||
### Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
||||
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
||||
stating that you have modified the software.
|
||||
|
||||
### No Other Rights
|
||||
|
||||
These terms do not imply any licenses other than those expressly granted in these terms.
|
||||
|
||||
### Termination
|
||||
|
||||
If you use the software in violation of these terms, such use is not licensed, and your license will
|
||||
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
||||
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
||||
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
||||
terms will cause your license to terminate automatically and permanently.
|
||||
|
||||
### No Liability
|
||||
|
||||
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
||||
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
||||
any kind of legal claim.
|
||||
|
||||
### Definitions
|
||||
|
||||
The “licensor” is the entity offering these terms.
|
||||
|
||||
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
||||
|
||||
“You” refers to the individual or entity agreeing to these terms.
|
||||
|
||||
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
||||
all organizations that have control over, are under the control of, or are under common control with that
|
||||
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
||||
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
“Your license” is the license granted to you for the software under these terms.
|
||||
|
||||
“Use” means anything you do with the software requiring your license.
|
||||
|
||||
“Trademark” means trademarks, service marks, and similar rights.
|
|
@ -1,27 +0,0 @@
|
|||
# The n8n Enterprise License (the “Enterprise License”)
|
||||
|
||||
Copyright (c) 2022-present n8n GmbH.
|
||||
|
||||
With regard to the n8n Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be used in production, if
|
||||
you (and any entity that you represent) hold a valid n8n Enterprise license corresponding to your
|
||||
usage. Subject to the foregoing sentence, you are free to modify this Software and publish patches
|
||||
to the Software. You agree that n8n and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications and/or patches
|
||||
may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid n8n
|
||||
Enterprise license for the corresponding usage. Notwithstanding the foregoing, you may copy and
|
||||
modify the Software for development and testing purposes, without requiring a subscription. You
|
||||
agree that n8n and/or its licensors (as applicable) retain all right, title and interest in and to
|
||||
all such modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or
|
||||
sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the n8n Software, those components are licensed
|
||||
under the original license provided by the owner of the applicable component.
|
|
@ -2,16 +2,6 @@
|
|||
"name": "n8n",
|
||||
"version": "1.49.0",
|
||||
"description": "n8n Workflow Automation Tool",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"main": "dist/index",
|
||||
"types": "dist/index.d.ts",
|
||||
"oclif": {
|
||||
|
|
|
@ -335,6 +335,7 @@ export class Server extends AbstractServer {
|
|||
// Route all UI urls to index.html to support history-api
|
||||
const nonUIRoutes: Readonly<string[]> = [
|
||||
'assets',
|
||||
'static',
|
||||
'types',
|
||||
'healthz',
|
||||
'metrics',
|
||||
|
@ -364,12 +365,20 @@ export class Server extends AbstractServer {
|
|||
next();
|
||||
}
|
||||
};
|
||||
const setCustomCacheHeader = (res: express.Response) => {
|
||||
if (/^\/types\/(nodes|credentials).json$/.test(res.req.url)) {
|
||||
res.setHeader('Cache-Control', 'no-cache, must-revalidate');
|
||||
}
|
||||
};
|
||||
|
||||
this.app.use(
|
||||
'/',
|
||||
express.static(staticCacheDir, cacheOptions),
|
||||
express.static(EDITOR_UI_DIST_DIR, cacheOptions),
|
||||
historyApiHandler,
|
||||
express.static(staticCacheDir, {
|
||||
...cacheOptions,
|
||||
setHeaders: setCustomCacheHeader,
|
||||
}),
|
||||
express.static(EDITOR_UI_DIST_DIR, cacheOptions),
|
||||
);
|
||||
} else {
|
||||
this.app.use('/', express.static(staticCacheDir, cacheOptions));
|
||||
|
|
|
@ -13,6 +13,8 @@ export class Column {
|
|||
|
||||
private defaultValue: unknown;
|
||||
|
||||
private primaryKeyConstraintName: string | undefined;
|
||||
|
||||
constructor(private name: string) {}
|
||||
|
||||
get bool() {
|
||||
|
@ -57,6 +59,12 @@ export class Column {
|
|||
return this;
|
||||
}
|
||||
|
||||
primaryWithName(name?: string) {
|
||||
this.isPrimary = true;
|
||||
this.primaryKeyConstraintName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
get notNull() {
|
||||
this.isNullable = false;
|
||||
return this;
|
||||
|
@ -74,12 +82,14 @@ export class Column {
|
|||
|
||||
// eslint-disable-next-line complexity
|
||||
toOptions(driver: Driver): TableColumnOptions {
|
||||
const { name, type, isNullable, isPrimary, isGenerated, length } = this;
|
||||
const { name, type, isNullable, isPrimary, isGenerated, length, primaryKeyConstraintName } =
|
||||
this;
|
||||
const isMysql = 'mysql' in driver;
|
||||
const isPostgres = 'postgres' in driver;
|
||||
const isSqlite = 'sqlite' in driver;
|
||||
|
||||
const options: TableColumnOptions = {
|
||||
primaryKeyConstraintName,
|
||||
name,
|
||||
isNullable,
|
||||
isPrimary,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn, RelationId } from '@n8n/typeorm';
|
||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from '@n8n/typeorm';
|
||||
import { ExecutionEntity } from './ExecutionEntity';
|
||||
|
||||
@Entity()
|
||||
|
@ -11,8 +11,8 @@ export class ExecutionMetadata {
|
|||
})
|
||||
execution: ExecutionEntity;
|
||||
|
||||
@RelationId((executionMetadata: ExecutionMetadata) => executionMetadata.execution)
|
||||
executionId: number;
|
||||
@Column()
|
||||
executionId: string;
|
||||
|
||||
@Column('text')
|
||||
key: string;
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
import type { MigrationContext, ReversibleMigration } from '@db/types';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export class AddConstraintToExecutionMetadata1720101653148 implements ReversibleMigration {
|
||||
async up(context: MigrationContext) {
|
||||
const { createTable, dropTable, column } = context.schemaBuilder;
|
||||
const { escape } = context;
|
||||
|
||||
const executionMetadataTableRaw = 'execution_metadata';
|
||||
const executionMetadataTable = escape.tableName(executionMetadataTableRaw);
|
||||
const executionMetadataTableTempRaw = 'execution_metadata_temp';
|
||||
const executionMetadataTableTemp = escape.tableName(executionMetadataTableTempRaw);
|
||||
const id = escape.columnName('id');
|
||||
const executionId = escape.columnName('executionId');
|
||||
const key = escape.columnName('key');
|
||||
const value = escape.columnName('value');
|
||||
|
||||
await createTable(executionMetadataTableTempRaw)
|
||||
.withColumns(
|
||||
column('id').int.notNull.primary.autoGenerate,
|
||||
column('executionId').int.notNull,
|
||||
// NOTE: This is a varchar(255) instead of text, because a unique index
|
||||
// on text is not supported on mysql, also why should we support
|
||||
// arbitrary length keys?
|
||||
column('key').varchar(255).notNull,
|
||||
column('value').text.notNull,
|
||||
)
|
||||
.withForeignKey('executionId', {
|
||||
tableName: 'execution_entity',
|
||||
columnName: 'id',
|
||||
onDelete: 'CASCADE',
|
||||
// In MySQL foreignKey names must be unique across all tables and
|
||||
// TypeORM creates predictable names based on the columnName.
|
||||
// So the temp table's foreignKey clashes with the current table's.
|
||||
name: context.isMysql ? nanoid() : undefined,
|
||||
})
|
||||
.withIndexOn(['executionId', 'key'], true);
|
||||
|
||||
if (context.isMysql) {
|
||||
await context.runQuery(`
|
||||
INSERT INTO ${executionMetadataTableTemp} (${id}, ${executionId}, ${key}, ${value})
|
||||
SELECT MAX(${id}) as ${id}, ${executionId}, ${key}, MAX(${value})
|
||||
FROM ${executionMetadataTable}
|
||||
GROUP BY ${executionId}, ${key}
|
||||
ON DUPLICATE KEY UPDATE
|
||||
id = IF(VALUES(${id}) > ${executionMetadataTableTemp}.${id}, VALUES(${id}), ${executionMetadataTableTemp}.${id}),
|
||||
value = IF(VALUES(${id}) > ${executionMetadataTableTemp}.${id}, VALUES(${value}), ${executionMetadataTableTemp}.${value});
|
||||
`);
|
||||
} else {
|
||||
await context.runQuery(`
|
||||
INSERT INTO ${executionMetadataTableTemp} (${id}, ${executionId}, ${key}, ${value})
|
||||
SELECT MAX(${id}) as ${id}, ${executionId}, ${key}, MAX(${value})
|
||||
FROM ${executionMetadataTable}
|
||||
GROUP BY ${executionId}, ${key}
|
||||
ON CONFLICT (${executionId}, ${key}) DO UPDATE SET
|
||||
id = EXCLUDED.id,
|
||||
value = EXCLUDED.value
|
||||
WHERE EXCLUDED.id > ${executionMetadataTableTemp}.id;
|
||||
`);
|
||||
}
|
||||
|
||||
await dropTable(executionMetadataTableRaw);
|
||||
await context.runQuery(
|
||||
`ALTER TABLE ${executionMetadataTableTemp} RENAME TO ${executionMetadataTable};`,
|
||||
);
|
||||
}
|
||||
|
||||
async down(context: MigrationContext) {
|
||||
const { createTable, dropTable, column } = context.schemaBuilder;
|
||||
const { escape } = context;
|
||||
|
||||
const executionMetadataTableRaw = 'execution_metadata';
|
||||
const executionMetadataTable = escape.tableName(executionMetadataTableRaw);
|
||||
const executionMetadataTableTempRaw = 'execution_metadata_temp';
|
||||
const executionMetadataTableTemp = escape.tableName(executionMetadataTableTempRaw);
|
||||
const id = escape.columnName('id');
|
||||
const executionId = escape.columnName('executionId');
|
||||
const key = escape.columnName('key');
|
||||
const value = escape.columnName('value');
|
||||
|
||||
await createTable(executionMetadataTableTempRaw)
|
||||
.withColumns(
|
||||
// INFO: The PK names that TypeORM creates are predictable and thus it
|
||||
// will create a PK name which already exists in the current
|
||||
// execution_metadata table. That's why we have to randomize the PK name
|
||||
// here.
|
||||
column('id').int.notNull.primaryWithName(nanoid()).autoGenerate,
|
||||
column('executionId').int.notNull,
|
||||
column('key').text.notNull,
|
||||
column('value').text.notNull,
|
||||
)
|
||||
.withForeignKey('executionId', {
|
||||
tableName: 'execution_entity',
|
||||
columnName: 'id',
|
||||
onDelete: 'CASCADE',
|
||||
// In MySQL foreignKey names must be unique across all tables and
|
||||
// TypeORM creates predictable names based on the columnName.
|
||||
// So the temp table's foreignKey clashes with the current table's.
|
||||
name: context.isMysql ? nanoid() : undefined,
|
||||
});
|
||||
|
||||
await context.runQuery(`
|
||||
INSERT INTO ${executionMetadataTableTemp} (${id}, ${executionId}, ${key}, ${value})
|
||||
SELECT ${id}, ${executionId}, ${key}, ${value} FROM ${executionMetadataTable};
|
||||
`);
|
||||
|
||||
await dropTable(executionMetadataTableRaw);
|
||||
await context.runQuery(
|
||||
`ALTER TABLE ${executionMetadataTableTemp} RENAME TO ${executionMetadataTable};`,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -58,6 +58,7 @@ import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-Move
|
|||
import { RemoveNodesAccess1712044305787 } from '../common/1712044305787-RemoveNodesAccess';
|
||||
import { MakeExecutionStatusNonNullable1714133768521 } from '../common/1714133768521-MakeExecutionStatusNonNullable';
|
||||
import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActivatedAtUserSetting';
|
||||
import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata';
|
||||
|
||||
export const mysqlMigrations: Migration[] = [
|
||||
InitialMigration1588157391238,
|
||||
|
@ -119,4 +120,5 @@ export const mysqlMigrations: Migration[] = [
|
|||
CreateProject1714133768519,
|
||||
MakeExecutionStatusNonNullable1714133768521,
|
||||
AddActivatedAtUserSetting1717498465931,
|
||||
AddConstraintToExecutionMetadata1720101653148,
|
||||
];
|
||||
|
|
|
@ -57,6 +57,7 @@ import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-Move
|
|||
import { RemoveNodesAccess1712044305787 } from '../common/1712044305787-RemoveNodesAccess';
|
||||
import { MakeExecutionStatusNonNullable1714133768521 } from '../common/1714133768521-MakeExecutionStatusNonNullable';
|
||||
import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActivatedAtUserSetting';
|
||||
import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata';
|
||||
|
||||
export const postgresMigrations: Migration[] = [
|
||||
InitialMigration1587669153312,
|
||||
|
@ -117,4 +118,5 @@ export const postgresMigrations: Migration[] = [
|
|||
CreateProject1714133768519,
|
||||
MakeExecutionStatusNonNullable1714133768521,
|
||||
AddActivatedAtUserSetting1717498465931,
|
||||
AddConstraintToExecutionMetadata1720101653148,
|
||||
];
|
||||
|
|
|
@ -55,6 +55,7 @@ import { MoveSshKeysToDatabase1711390882123 } from '../common/1711390882123-Move
|
|||
import { RemoveNodesAccess1712044305787 } from '../common/1712044305787-RemoveNodesAccess';
|
||||
import { MakeExecutionStatusNonNullable1714133768521 } from '../common/1714133768521-MakeExecutionStatusNonNullable';
|
||||
import { AddActivatedAtUserSetting1717498465931 } from './1717498465931-AddActivatedAtUserSetting';
|
||||
import { AddConstraintToExecutionMetadata1720101653148 } from '../common/1720101653148-AddConstraintToExecutionMetadata';
|
||||
|
||||
const sqliteMigrations: Migration[] = [
|
||||
InitialMigration1588102412422,
|
||||
|
@ -113,6 +114,7 @@ const sqliteMigrations: Migration[] = [
|
|||
CreateProject1714133768519,
|
||||
MakeExecutionStatusNonNullable1714133768521,
|
||||
AddActivatedAtUserSetting1717498465931,
|
||||
AddConstraintToExecutionMetadata1720101653148,
|
||||
];
|
||||
|
||||
export { sqliteMigrations };
|
||||
|
|
|
@ -235,6 +235,7 @@ describe('ExecutionService', () => {
|
|||
* Assert
|
||||
*/
|
||||
expect(waitTracker.stopExecution).not.toHaveBeenCalled();
|
||||
expect(activeExecutions.stopExecution).toHaveBeenCalled();
|
||||
expect(queue.findRunningJobBy).toBeCalledWith({ executionId: execution.id });
|
||||
expect(queue.stopJob).toHaveBeenCalled();
|
||||
expect(executionRepository.stopDuringRun).toHaveBeenCalled();
|
||||
|
@ -260,6 +261,7 @@ describe('ExecutionService', () => {
|
|||
* Assert
|
||||
*/
|
||||
expect(waitTracker.stopExecution).toHaveBeenCalledWith(execution.id);
|
||||
expect(activeExecutions.stopExecution).toHaveBeenCalled();
|
||||
expect(queue.findRunningJobBy).toBeCalledWith({ executionId: execution.id });
|
||||
expect(queue.stopJob).toHaveBeenCalled();
|
||||
expect(executionRepository.stopDuringRun).toHaveBeenCalled();
|
||||
|
|
|
@ -460,6 +460,10 @@ export class ExecutionService {
|
|||
return await this.stopInRegularMode(execution);
|
||||
}
|
||||
|
||||
if (this.activeExecutions.has(execution.id)) {
|
||||
await this.activeExecutions.stopExecution(execution.id);
|
||||
}
|
||||
|
||||
if (this.waitTracker.has(execution.id)) {
|
||||
await this.waitTracker.stopExecution(execution.id);
|
||||
}
|
||||
|
|
|
@ -6,19 +6,18 @@ import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata';
|
|||
export class ExecutionMetadataService {
|
||||
constructor(private readonly executionMetadataRepository: ExecutionMetadataRepository) {}
|
||||
|
||||
async save(
|
||||
executionId: string,
|
||||
executionMetadata: Record<string, string>,
|
||||
): Promise<ExecutionMetadata[]> {
|
||||
const metadataRows = [];
|
||||
async save(executionId: string, executionMetadata: Record<string, string>): Promise<void> {
|
||||
const metadataRows: Array<Pick<ExecutionMetadata, 'executionId' | 'key' | 'value'>> = [];
|
||||
for (const [key, value] of Object.entries(executionMetadata)) {
|
||||
metadataRows.push({
|
||||
execution: { id: executionId },
|
||||
executionId,
|
||||
key,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
return await this.executionMetadataRepository.save(metadataRows);
|
||||
await this.executionMetadataRepository.upsert(metadataRows, {
|
||||
conflictPaths: { executionId: true, key: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import * as testDb from '../shared/testDb';
|
||||
import Container from 'typedi';
|
||||
import { ExecutionMetadataRepository } from '@/databases/repositories/executionMetadata.repository';
|
||||
import { ExecutionMetadataService } from '@/services/executionMetadata.service';
|
||||
import { createExecution } from '@test-integration/db/executions';
|
||||
import { createWorkflow } from '@test-integration/db/workflows';
|
||||
|
||||
let executionMetadataRepository: ExecutionMetadataRepository;
|
||||
let executionMetadataService: ExecutionMetadataService;
|
||||
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
|
||||
executionMetadataRepository = Container.get(ExecutionMetadataRepository);
|
||||
executionMetadataService = Container.get(ExecutionMetadataService);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await testDb.truncate(['User']);
|
||||
});
|
||||
|
||||
describe('ProjectService', () => {
|
||||
describe('save', () => {
|
||||
it('should deduplicate entries by exeuctionId and key, keeping the latest one', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflow = await createWorkflow();
|
||||
const execution = await createExecution({}, workflow);
|
||||
const key = 'key';
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await executionMetadataService.save(execution.id, { [key]: value1 });
|
||||
await executionMetadataService.save(execution.id, { [key]: value2 });
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const rows = await executionMetadataRepository.find({
|
||||
where: { executionId: execution.id, key },
|
||||
});
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toHaveProperty('value', value2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -15,20 +15,26 @@ describe('ExecutionMetadataService', () => {
|
|||
|
||||
await Container.get(ExecutionMetadataService).save(executionId, toSave);
|
||||
|
||||
expect(repository.save).toHaveBeenCalledTimes(1);
|
||||
expect(repository.save.mock.calls[0]).toEqual([
|
||||
expect(repository.upsert).toHaveBeenCalledTimes(1);
|
||||
expect(repository.upsert.mock.calls[0]).toEqual([
|
||||
[
|
||||
{
|
||||
execution: { id: executionId },
|
||||
executionId,
|
||||
key: 'test1',
|
||||
value: 'value1',
|
||||
},
|
||||
{
|
||||
execution: { id: executionId },
|
||||
executionId,
|
||||
key: 'test2',
|
||||
value: 'value2',
|
||||
},
|
||||
],
|
||||
{
|
||||
conflictPaths: {
|
||||
executionId: true,
|
||||
key: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
# License
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- Content of branches other than the main branch (i.e. "master") are not licensed.
|
||||
- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License.
|
||||
To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License
|
||||
specifically allowing you access to such source code files and as defined in "LICENSE_EE.md".
|
||||
- All third party components incorporated into the n8n Software are licensed under the original license
|
||||
provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
|
||||
License" as defined below.
|
||||
|
||||
## Sustainable Use License
|
||||
|
||||
Version 1.0
|
||||
|
||||
### Acceptance
|
||||
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
### Copyright License
|
||||
|
||||
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
||||
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
||||
to the limitations below.
|
||||
|
||||
### Limitations
|
||||
|
||||
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
||||
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
||||
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
||||
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
||||
|
||||
### Patents
|
||||
|
||||
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
||||
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
||||
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
||||
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
||||
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
||||
license ends immediately for work on behalf of your company.
|
||||
|
||||
### Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
||||
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
||||
stating that you have modified the software.
|
||||
|
||||
### No Other Rights
|
||||
|
||||
These terms do not imply any licenses other than those expressly granted in these terms.
|
||||
|
||||
### Termination
|
||||
|
||||
If you use the software in violation of these terms, such use is not licensed, and your license will
|
||||
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
||||
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
||||
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
||||
terms will cause your license to terminate automatically and permanently.
|
||||
|
||||
### No Liability
|
||||
|
||||
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
||||
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
||||
any kind of legal claim.
|
||||
|
||||
### Definitions
|
||||
|
||||
The “licensor” is the entity offering these terms.
|
||||
|
||||
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
||||
|
||||
“You” refers to the individual or entity agreeing to these terms.
|
||||
|
||||
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
||||
all organizations that have control over, are under the control of, or are under common control with that
|
||||
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
||||
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
“Your license” is the license granted to you for the software under these terms.
|
||||
|
||||
“Use” means anything you do with the software requiring your license.
|
||||
|
||||
“Trademark” means trademarks, service marks, and similar rights.
|
|
@ -1,27 +0,0 @@
|
|||
# The n8n Enterprise License (the “Enterprise License”)
|
||||
|
||||
Copyright (c) 2022-present n8n GmbH.
|
||||
|
||||
With regard to the n8n Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be used in production, if
|
||||
you (and any entity that you represent) hold a valid n8n Enterprise license corresponding to your
|
||||
usage. Subject to the foregoing sentence, you are free to modify this Software and publish patches
|
||||
to the Software. You agree that n8n and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications and/or patches
|
||||
may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid n8n
|
||||
Enterprise license for the corresponding usage. Notwithstanding the foregoing, you may copy and
|
||||
modify the Software for development and testing purposes, without requiring a subscription. You
|
||||
agree that n8n and/or its licensors (as applicable) retain all right, title and interest in and to
|
||||
all such modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or
|
||||
sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the n8n Software, those components are licensed
|
||||
under the original license provided by the owner of the applicable component.
|
|
@ -2,16 +2,6 @@
|
|||
"name": "n8n-core",
|
||||
"version": "1.49.0",
|
||||
"description": "Core functionality of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"main": "dist/index",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
# License
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- Content of branches other than the main branch (i.e. "master") are not licensed.
|
||||
- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License.
|
||||
To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License
|
||||
specifically allowing you access to such source code files and as defined in "LICENSE_EE.md".
|
||||
- All third party components incorporated into the n8n Software are licensed under the original license
|
||||
provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
|
||||
License" as defined below.
|
||||
|
||||
## Sustainable Use License
|
||||
|
||||
Version 1.0
|
||||
|
||||
### Acceptance
|
||||
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
### Copyright License
|
||||
|
||||
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
||||
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
||||
to the limitations below.
|
||||
|
||||
### Limitations
|
||||
|
||||
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
||||
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
||||
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
||||
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
||||
|
||||
### Patents
|
||||
|
||||
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
||||
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
||||
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
||||
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
||||
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
||||
license ends immediately for work on behalf of your company.
|
||||
|
||||
### Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
||||
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
||||
stating that you have modified the software.
|
||||
|
||||
### No Other Rights
|
||||
|
||||
These terms do not imply any licenses other than those expressly granted in these terms.
|
||||
|
||||
### Termination
|
||||
|
||||
If you use the software in violation of these terms, such use is not licensed, and your license will
|
||||
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
||||
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
||||
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
||||
terms will cause your license to terminate automatically and permanently.
|
||||
|
||||
### No Liability
|
||||
|
||||
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
||||
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
||||
any kind of legal claim.
|
||||
|
||||
### Definitions
|
||||
|
||||
The “licensor” is the entity offering these terms.
|
||||
|
||||
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
||||
|
||||
“You” refers to the individual or entity agreeing to these terms.
|
||||
|
||||
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
||||
all organizations that have control over, are under the control of, or are under common control with that
|
||||
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
||||
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
“Your license” is the license granted to you for the software under these terms.
|
||||
|
||||
“Use” means anything you do with the software requiring your license.
|
||||
|
||||
“Trademark” means trademarks, service marks, and similar rights.
|
|
@ -1,27 +0,0 @@
|
|||
# The n8n Enterprise License (the “Enterprise License”)
|
||||
|
||||
Copyright (c) 2022-present n8n GmbH.
|
||||
|
||||
With regard to the n8n Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be used in production, if
|
||||
you (and any entity that you represent) hold a valid n8n Enterprise license corresponding to your
|
||||
usage. Subject to the foregoing sentence, you are free to modify this Software and publish patches
|
||||
to the Software. You agree that n8n and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications and/or patches
|
||||
may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid n8n
|
||||
Enterprise license for the corresponding usage. Notwithstanding the foregoing, you may copy and
|
||||
modify the Software for development and testing purposes, without requiring a subscription. You
|
||||
agree that n8n and/or its licensors (as applicable) retain all right, title and interest in and to
|
||||
all such modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or
|
||||
sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the n8n Software, those components are licensed
|
||||
under the original license provided by the owner of the applicable component.
|
|
@ -1,18 +1,8 @@
|
|||
{
|
||||
"name": "n8n-design-system",
|
||||
"version": "1.39.0",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Mutasem Aldmour",
|
||||
"email": "mutasem@n8n.io"
|
||||
},
|
||||
"main": "src/main.ts",
|
||||
"import": "src/main.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "pnpm run storybook",
|
||||
"clean": "rimraf dist .turbo",
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
--color-node-executing-background: var(--color-primary-tint-3);
|
||||
--color-node-executing-other-background: var(--color-primary-tint-3);
|
||||
--color-node-pinned-border: var(--color-secondary);
|
||||
--color-node-running-border: var(--color-primary);
|
||||
--node-type-main-color: var(--prim-gray-490);
|
||||
|
||||
// Sticky
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/src
|
||||
/tests
|
||||
/.turbo
|
||||
!dist
|
||||
|
||||
.browserslistrc
|
||||
|
@ -8,3 +9,4 @@ postcss.config.js
|
|||
vue.config.js
|
||||
dist/report.html
|
||||
public/
|
||||
.eslintrc.js
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
# License
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- Content of branches other than the main branch (i.e. "master") are not licensed.
|
||||
- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License.
|
||||
To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License
|
||||
specifically allowing you access to such source code files and as defined in "LICENSE_EE.md".
|
||||
- All third party components incorporated into the n8n Software are licensed under the original license
|
||||
provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
|
||||
License" as defined below.
|
||||
|
||||
## Sustainable Use License
|
||||
|
||||
Version 1.0
|
||||
|
||||
### Acceptance
|
||||
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
### Copyright License
|
||||
|
||||
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
||||
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
||||
to the limitations below.
|
||||
|
||||
### Limitations
|
||||
|
||||
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
||||
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
||||
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
||||
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
||||
|
||||
### Patents
|
||||
|
||||
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
||||
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
||||
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
||||
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
||||
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
||||
license ends immediately for work on behalf of your company.
|
||||
|
||||
### Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
||||
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
||||
stating that you have modified the software.
|
||||
|
||||
### No Other Rights
|
||||
|
||||
These terms do not imply any licenses other than those expressly granted in these terms.
|
||||
|
||||
### Termination
|
||||
|
||||
If you use the software in violation of these terms, such use is not licensed, and your license will
|
||||
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
||||
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
||||
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
||||
terms will cause your license to terminate automatically and permanently.
|
||||
|
||||
### No Liability
|
||||
|
||||
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
||||
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
||||
any kind of legal claim.
|
||||
|
||||
### Definitions
|
||||
|
||||
The “licensor” is the entity offering these terms.
|
||||
|
||||
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
||||
|
||||
“You” refers to the individual or entity agreeing to these terms.
|
||||
|
||||
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
||||
all organizations that have control over, are under the control of, or are under common control with that
|
||||
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
||||
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
“Your license” is the license granted to you for the software under these terms.
|
||||
|
||||
“Use” means anything you do with the software requiring your license.
|
||||
|
||||
“Trademark” means trademarks, service marks, and similar rights.
|
|
@ -1,27 +0,0 @@
|
|||
# The n8n Enterprise License (the “Enterprise License”)
|
||||
|
||||
Copyright (c) 2022-present n8n GmbH.
|
||||
|
||||
With regard to the n8n Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be used in production, if
|
||||
you (and any entity that you represent) hold a valid n8n Enterprise license corresponding to your
|
||||
usage. Subject to the foregoing sentence, you are free to modify this Software and publish patches
|
||||
to the Software. You agree that n8n and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications and/or patches
|
||||
may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid n8n
|
||||
Enterprise license for the corresponding usage. Notwithstanding the foregoing, you may copy and
|
||||
modify the Software for development and testing purposes, without requiring a subscription. You
|
||||
agree that n8n and/or its licensors (as applicable) retain all right, title and interest in and to
|
||||
all such modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or
|
||||
sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the n8n Software, those components are licensed
|
||||
under the original license provided by the owner of the applicable component.
|
|
@ -2,17 +2,7 @@
|
|||
"name": "n8n-editor-ui",
|
||||
"version": "1.49.0",
|
||||
"description": "Workflow Editor UI for n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
},
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"build": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||
|
|
|
@ -10,11 +10,14 @@ export function createCanvasNodeData({
|
|||
inputs = [],
|
||||
outputs = [],
|
||||
connections = { input: {}, output: {} },
|
||||
execution = {},
|
||||
execution = { running: false },
|
||||
issues = { items: [], visible: false },
|
||||
pinnedData = { count: 0, visible: false },
|
||||
runData = { count: 0, visible: false },
|
||||
renderType = 'default',
|
||||
render = {
|
||||
type: 'default',
|
||||
options: { configurable: false, configuration: false, trigger: false },
|
||||
},
|
||||
}: Partial<CanvasElementData> = {}): CanvasElementData {
|
||||
return {
|
||||
execution,
|
||||
|
@ -28,7 +31,7 @@ export function createCanvasNodeData({
|
|||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
renderType,
|
||||
render,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -55,7 +58,7 @@ export function createCanvasNodeProps({
|
|||
label = 'Test Node',
|
||||
selected = false,
|
||||
data = {},
|
||||
} = {}) {
|
||||
}: { id?: string; label?: string; selected?: boolean; data?: Partial<CanvasElementData> } = {}) {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
|
@ -69,7 +72,7 @@ export function createCanvasNodeProvide({
|
|||
label = 'Test Node',
|
||||
selected = false,
|
||||
data = {},
|
||||
} = {}) {
|
||||
}: { id?: string; label?: string; selected?: boolean; data?: Partial<CanvasElementData> } = {}) {
|
||||
const props = createCanvasNodeProps({ id, label, selected, data });
|
||||
return {
|
||||
[`${CanvasNodeKey}`]: {
|
||||
|
|
|
@ -107,6 +107,7 @@ describe('Canvas', () => {
|
|||
});
|
||||
await fireEvent.mouseUp(node, { view: window });
|
||||
|
||||
expect(emitted()['update:node:position']).toEqual([['1', { x: 100, y: 100 }]]);
|
||||
// Snap to 16px grid: 100 -> 96
|
||||
expect(emitted()['update:node:position']).toEqual([['1', { x: 96, y: 96 }]]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import type { CanvasConnection, CanvasElement } from '@/types';
|
||||
import type { CanvasConnection, CanvasElement, ConnectStartEvent } from '@/types';
|
||||
import type { EdgeMouseEvent, NodeDragEvent, Connection, XYPosition } from '@vue-flow/core';
|
||||
import { useVueFlow, VueFlow, PanelPosition } from '@vue-flow/core';
|
||||
import { Background } from '@vue-flow/background';
|
||||
|
@ -17,9 +17,13 @@ const emit = defineEmits<{
|
|||
'update:node:active': [id: string];
|
||||
'update:node:enabled': [id: string];
|
||||
'update:node:selected': [id?: string];
|
||||
'run:node': [id: string];
|
||||
'delete:node': [id: string];
|
||||
'delete:connection': [connection: Connection];
|
||||
'create:connection:start': [handle: ConnectStartEvent];
|
||||
'create:connection': [connection: Connection];
|
||||
'create:connection:end': [connection: Connection];
|
||||
'create:connection:cancelled': [handle: ConnectStartEvent];
|
||||
'click:pane': [position: XYPosition];
|
||||
}>();
|
||||
|
||||
|
@ -79,12 +83,48 @@ function onDeleteNode(id: string) {
|
|||
emit('delete:node', id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connections
|
||||
*/
|
||||
|
||||
const connectionCreated = ref(false);
|
||||
const connectionEventData = ref<ConnectStartEvent | Connection>();
|
||||
|
||||
const isConnection = (data: ConnectStartEvent | Connection | undefined): data is Connection =>
|
||||
!!data && connectionCreated.value;
|
||||
|
||||
const isConnectionCancelled = (
|
||||
data: ConnectStartEvent | Connection | undefined,
|
||||
): data is ConnectStartEvent => !!data && !connectionCreated.value;
|
||||
|
||||
function onConnectStart(handle: ConnectStartEvent) {
|
||||
emit('create:connection:start', handle);
|
||||
|
||||
connectionEventData.value = handle;
|
||||
connectionCreated.value = false;
|
||||
}
|
||||
|
||||
function onConnect(connection: Connection) {
|
||||
emit('create:connection', connection);
|
||||
|
||||
connectionEventData.value = connection;
|
||||
connectionCreated.value = true;
|
||||
}
|
||||
|
||||
function onConnectEnd() {
|
||||
if (isConnection(connectionEventData.value)) {
|
||||
emit('create:connection:end', connectionEventData.value);
|
||||
} else if (isConnectionCancelled(connectionEventData.value)) {
|
||||
emit('create:connection:cancelled', connectionEventData.value);
|
||||
}
|
||||
}
|
||||
|
||||
function onDeleteConnection(connection: Connection) {
|
||||
emit('delete:connection', connection);
|
||||
}
|
||||
|
||||
function onConnect(...args: unknown[]) {
|
||||
emit('create:connection', args[0] as Connection);
|
||||
function onRunNode(id: string) {
|
||||
emit('run:node', id);
|
||||
}
|
||||
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
|
@ -121,6 +161,8 @@ function onClickPane(event: MouseEvent) {
|
|||
:apply-changes="false"
|
||||
fit-view-on-init
|
||||
pan-on-scroll
|
||||
snap-to-grid
|
||||
:snap-grid="[16, 16]"
|
||||
:min-zoom="0.2"
|
||||
:max-zoom="2"
|
||||
data-test-id="canvas"
|
||||
|
@ -128,13 +170,16 @@ function onClickPane(event: MouseEvent) {
|
|||
@selection-drag-stop="onSelectionDragStop"
|
||||
@edge-mouse-enter="onMouseEnterEdge"
|
||||
@edge-mouse-leave="onMouseLeaveEdge"
|
||||
@pane-click="onClickPane"
|
||||
@connect-start="onConnectStart"
|
||||
@connect="onConnect"
|
||||
@connect-end="onConnectEnd"
|
||||
@pane-click="onClickPane"
|
||||
>
|
||||
<template #node-canvas-node="canvasNodeProps">
|
||||
<CanvasNode
|
||||
v-bind="canvasNodeProps"
|
||||
@delete="onDeleteNode"
|
||||
@run="onRunNode"
|
||||
@select="onSelectNode"
|
||||
@toggle="onToggleNodeEnabled"
|
||||
@activate="onSetNodeActive"
|
||||
|
|
|
@ -47,30 +47,4 @@ const { elements, connections } = useCanvasMapping({ workflow, workflowObject })
|
|||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.executionButtons {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: var(--spacing-l);
|
||||
width: auto;
|
||||
|
||||
@media (max-width: $breakpoint-2xs) {
|
||||
bottom: 150px;
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-left: 0.625rem;
|
||||
|
||||
&:first-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import CanvasExecuteWorkflowButton from './CanvasExecuteWorkflowButton.vue';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasExecuteWorkflowButton);
|
||||
|
||||
describe('CanvasExecuteWorkflowButton', () => {
|
||||
it('should render correctly', () => {
|
||||
const wrapper = renderComponent();
|
||||
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render different label when executing', () => {
|
||||
const wrapper = renderComponent({
|
||||
props: {
|
||||
executing: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.getAllByText('Executing workflow')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should render different label when executing and waiting for webhook', () => {
|
||||
const wrapper = renderComponent({
|
||||
props: {
|
||||
executing: true,
|
||||
waitingForWebhook: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.getAllByText('Waiting for trigger event')).toHaveLength(2);
|
||||
});
|
||||
});
|
|
@ -2,31 +2,36 @@
|
|||
import KeyboardShortcutTooltip from '@/components/KeyboardShortcutTooltip.vue';
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
|
||||
defineEmits<{
|
||||
click: [event: MouseEvent];
|
||||
}>();
|
||||
|
||||
const uiStore = useUIStore();
|
||||
const locale = useI18n();
|
||||
const props = defineProps<{
|
||||
waitingForWebhook: boolean;
|
||||
executing: boolean;
|
||||
}>();
|
||||
|
||||
const workflowRunning = computed(() => uiStore.isActionActive['workflowRunning']);
|
||||
const i18n = useI18n();
|
||||
|
||||
const runButtonText = computed(() => {
|
||||
if (!workflowRunning.value) {
|
||||
return locale.baseText('nodeView.runButtonText.executeWorkflow');
|
||||
const label = computed(() => {
|
||||
if (!props.executing) {
|
||||
return i18n.baseText('nodeView.runButtonText.executeWorkflow');
|
||||
}
|
||||
|
||||
return locale.baseText('nodeView.runButtonText.executingWorkflow');
|
||||
if (props.waitingForWebhook) {
|
||||
return i18n.baseText('nodeView.runButtonText.waitingForTriggerEvent');
|
||||
}
|
||||
|
||||
return i18n.baseText('nodeView.runButtonText.executingWorkflow');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<KeyboardShortcutTooltip :label="runButtonText" :shortcut="{ metaKey: true, keys: ['↵'] }">
|
||||
<KeyboardShortcutTooltip :label="label" :shortcut="{ metaKey: true, keys: ['↵'] }">
|
||||
<N8nButton
|
||||
:loading="workflowRunning"
|
||||
:label="runButtonText"
|
||||
:loading="executing"
|
||||
:label="label"
|
||||
size="large"
|
||||
icon="flask"
|
||||
type="primary"
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import CanvasStopCurrentExecutionButton from './CanvasStopCurrentExecutionButton.vue';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasStopCurrentExecutionButton);
|
||||
|
||||
describe('CanvasStopCurrentExecutionButton', () => {
|
||||
it('should render correctly', () => {
|
||||
const wrapper = renderComponent();
|
||||
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render different title when loading', () => {
|
||||
const wrapper = renderComponent({
|
||||
props: {
|
||||
stopping: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.getByTitle('Stopping current execution')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts" setup>
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
stopping?: boolean;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const title = computed(() =>
|
||||
props.stopping
|
||||
? i18n.baseText('nodeView.stoppingCurrentExecution')
|
||||
: i18n.baseText('nodeView.stopCurrentExecution'),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n8n-icon-button
|
||||
icon="stop"
|
||||
size="large"
|
||||
class="stop-execution"
|
||||
type="secondary"
|
||||
:title="title"
|
||||
:loading="stopping"
|
||||
data-test-id="stop-execution-button"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,12 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import CanvasStopWaitingForWebhookButton from './CanvasStopWaitingForWebhookButton.vue';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasStopWaitingForWebhookButton);
|
||||
|
||||
describe('CanvasStopCurrentExecutionButton', () => {
|
||||
it('should render correctly', () => {
|
||||
const wrapper = renderComponent();
|
||||
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts" setup>
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
|
||||
const i18n = useI18n();
|
||||
</script>
|
||||
<template>
|
||||
<n8n-icon-button
|
||||
class="stop-execution"
|
||||
icon="stop"
|
||||
size="large"
|
||||
:title="i18n.baseText('nodeView.stopWaitingForWebhookCall')"
|
||||
type="secondary"
|
||||
data-test-id="stop-execution-waiting-for-webhook-button"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,7 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`CanvasExecuteWorkflowButton > should render correctly 1`] = `
|
||||
"<button class="button button primary large withIcon el-tooltip__trigger el-tooltip__trigger" aria-live="polite" data-test-id="execute-workflow-button"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-flask fa-w-14 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="flask" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path class="" fill="currentColor" d="M437.2 403.5L320 215V64h8c13.3 0 24-10.7 24-24V24c0-13.3-10.7-24-24-24H120c-13.3 0-24 10.7-24 24v16c0 13.3 10.7 24 24 24h8v151L10.8 403.5C-18.5 450.6 15.3 512 70.9 512h306.2c55.7 0 89.4-61.5 60.1-108.5zM137.9 320l48.2-77.6c3.7-5.2 5.8-11.6 5.8-18.4V64h64v160c0 6.9 2.2 13.2 5.8 18.4l48.2 77.6h-172z"></path></svg></span></span><span>Test workflow</span></button>
|
||||
<!--teleport start-->
|
||||
<!--teleport end-->"
|
||||
`;
|
|
@ -0,0 +1,7 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`CanvasStopCurrentExecutionButton > should render correctly 1`] = `
|
||||
"<button class="button button secondary large withIcon square stop-execution stop-execution" aria-live="polite" title="Stop current execution" data-test-id="stop-execution-button"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-stop fa-w-14 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="stop" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path class="" fill="currentColor" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z"></path></svg></span></span>
|
||||
<!--v-if-->
|
||||
</button>"
|
||||
`;
|
|
@ -0,0 +1,7 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`CanvasStopCurrentExecutionButton > should render correctly 1`] = `
|
||||
"<button class="button button secondary large withIcon square stop-execution stop-execution" aria-live="polite" title="Stop waiting for webhook call" data-test-id="stop-execution-waiting-for-webhook-button"><span class="icon"><span class="n8n-text compact size-large regular n8n-icon n8n-icon"><svg class="svg-inline--fa fa-stop fa-w-14 large" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="stop" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path class="" fill="currentColor" d="M400 32H48C21.5 32 0 53.5 0 80v352c0 26.5 21.5 48 48 48h352c26.5 0 48-21.5 48-48V80c0-26.5-21.5-48-48-48z"></path></svg></span></span>
|
||||
<!--v-if-->
|
||||
</button>"
|
||||
`;
|
|
@ -1,21 +1,25 @@
|
|||
import { fireEvent } from '@testing-library/vue';
|
||||
import CanvasEdge from './CanvasEdge.vue';
|
||||
import CanvasEdge, { type CanvasEdgeProps } from './CanvasEdge.vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { Position } from '@vue-flow/core';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasEdge, {
|
||||
props: {
|
||||
sourceX: 0,
|
||||
sourceY: 0,
|
||||
sourcePosition: 'top',
|
||||
targetX: 100,
|
||||
targetY: 100,
|
||||
targetPosition: 'bottom',
|
||||
data: {
|
||||
status: undefined,
|
||||
},
|
||||
const DEFAULT_PROPS = {
|
||||
sourceX: 0,
|
||||
sourceY: 0,
|
||||
sourcePosition: Position.Top,
|
||||
targetX: 100,
|
||||
targetY: 100,
|
||||
targetPosition: Position.Bottom,
|
||||
data: {
|
||||
status: undefined,
|
||||
source: { index: 0, type: 'main' },
|
||||
target: { index: 0, type: 'main' },
|
||||
},
|
||||
} satisfies Partial<CanvasEdgeProps>;
|
||||
const renderComponent = createComponentRenderer(CanvasEdge, {
|
||||
props: DEFAULT_PROPS,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -42,4 +46,28 @@ describe('CanvasEdge', () => {
|
|||
stroke: 'var(--color-foreground-xdark)',
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly style a running connection', () => {
|
||||
const { container } = renderComponent({
|
||||
props: { ...DEFAULT_PROPS, data: { ...DEFAULT_PROPS.data, status: 'running' } },
|
||||
});
|
||||
|
||||
const edge = container.querySelector('.vue-flow__edge-path');
|
||||
|
||||
expect(edge).toHaveStyle({
|
||||
stroke: 'var(--color-primary)',
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly style a pinned connection', () => {
|
||||
const { container } = renderComponent({
|
||||
props: { ...DEFAULT_PROPS, data: { ...DEFAULT_PROPS.data, status: 'pinned' } },
|
||||
});
|
||||
|
||||
const edge = container.querySelector('.vue-flow__edge-path');
|
||||
|
||||
expect(edge).toHaveStyle({
|
||||
stroke: 'var(--color-secondary)',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,16 +4,17 @@ import type { Connection, EdgeProps } from '@vue-flow/core';
|
|||
import { BaseEdge, EdgeLabelRenderer, getBezierPath } from '@vue-flow/core';
|
||||
import CanvasEdgeToolbar from './CanvasEdgeToolbar.vue';
|
||||
import { computed, useCssModule } from 'vue';
|
||||
import type { CanvasConnectionData } from '@/types';
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [connection: Connection];
|
||||
}>();
|
||||
|
||||
const props = defineProps<
|
||||
EdgeProps & {
|
||||
hovered?: boolean;
|
||||
}
|
||||
>();
|
||||
export type CanvasEdgeProps = EdgeProps<CanvasConnectionData> & {
|
||||
hovered?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<CanvasEdgeProps>();
|
||||
|
||||
const $style = useCssModule();
|
||||
|
||||
|
@ -27,6 +28,8 @@ const statusColor = computed(() => {
|
|||
return 'var(--color-success)';
|
||||
} else if (status.value === 'pinned') {
|
||||
return 'var(--color-secondary)';
|
||||
} else if (status.value === 'running') {
|
||||
return 'var(--color-primary)';
|
||||
} else {
|
||||
return 'var(--color-foreground-xdark)';
|
||||
}
|
||||
|
|
|
@ -53,11 +53,14 @@ describe('CanvasNode', () => {
|
|||
...createCanvasNodeProps({
|
||||
data: {
|
||||
inputs: [
|
||||
{ type: NodeConnectionType.Main },
|
||||
{ type: NodeConnectionType.Main },
|
||||
{ type: NodeConnectionType.Main },
|
||||
{ type: NodeConnectionType.Main, index: 0 },
|
||||
{ type: NodeConnectionType.Main, index: 0 },
|
||||
{ type: NodeConnectionType.Main, index: 0 },
|
||||
],
|
||||
outputs: [
|
||||
{ type: NodeConnectionType.Main, index: 0 },
|
||||
{ type: NodeConnectionType.Main, index: 0 },
|
||||
],
|
||||
outputs: [{ type: NodeConnectionType.Main }, { type: NodeConnectionType.Main }],
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -17,6 +17,7 @@ import type { NodeProps } from '@vue-flow/core';
|
|||
|
||||
const emit = defineEmits<{
|
||||
delete: [id: string];
|
||||
run: [id: string];
|
||||
select: [id: string, selected: boolean];
|
||||
toggle: [id: string];
|
||||
activate: [id: string];
|
||||
|
@ -110,6 +111,10 @@ function onDelete() {
|
|||
emit('delete', props.id);
|
||||
}
|
||||
|
||||
function onRun() {
|
||||
emit('run', props.id);
|
||||
}
|
||||
|
||||
function onDisabledToggle() {
|
||||
emit('toggle', props.id);
|
||||
}
|
||||
|
@ -151,6 +156,7 @@ function onActivate() {
|
|||
:class="$style.canvasNodeToolbar"
|
||||
@delete="onDelete"
|
||||
@toggle="onDisabledToggle"
|
||||
@run="onRun"
|
||||
/>
|
||||
|
||||
<CanvasNodeRenderer v-if="nodeType" @dblclick="onActivate">
|
||||
|
@ -164,19 +170,21 @@ function onActivate() {
|
|||
.canvasNode {
|
||||
&:hover {
|
||||
.canvasNodeToolbar {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.canvasNodeToolbar {
|
||||
display: none;
|
||||
transition: opacity 0.1s ease-in;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -100%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.canvasNodeToolbar:focus-within {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -21,7 +21,7 @@ describe('CanvasNodeRenderer', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('canvas-node-default')).toBeInTheDocument();
|
||||
expect(getByTestId('canvas-default-node')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render configuration node correctly', async () => {
|
||||
|
@ -30,14 +30,17 @@ describe('CanvasNodeRenderer', () => {
|
|||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
renderType: 'configuration',
|
||||
render: {
|
||||
type: 'default',
|
||||
options: { configuration: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('canvas-node-configuration')).toBeInTheDocument();
|
||||
expect(getByTestId('canvas-configuration-node')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render configurable node correctly', async () => {
|
||||
|
@ -46,13 +49,16 @@ describe('CanvasNodeRenderer', () => {
|
|||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
renderType: 'configurable',
|
||||
render: {
|
||||
type: 'default',
|
||||
options: { configurable: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('canvas-node-configurable')).toBeInTheDocument();
|
||||
expect(getByTestId('canvas-configurable-node')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<script lang="ts" setup>
|
||||
import { h, inject } from 'vue';
|
||||
import CanvasNodeDefault from '@/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue';
|
||||
import CanvasNodeConfiguration from '@/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.vue';
|
||||
import CanvasNodeConfigurable from '@/components/canvas/elements/nodes/render-types/CanvasNodeConfigurable.vue';
|
||||
import { CanvasNodeKey } from '@/constants';
|
||||
|
||||
const node = inject(CanvasNodeKey);
|
||||
|
@ -13,19 +11,8 @@ const slots = defineSlots<{
|
|||
|
||||
const Render = () => {
|
||||
let Component;
|
||||
switch (node?.data.value.renderType) {
|
||||
case 'configurable':
|
||||
Component = CanvasNodeConfigurable;
|
||||
break;
|
||||
|
||||
case 'configuration':
|
||||
Component = CanvasNodeConfiguration;
|
||||
break;
|
||||
|
||||
case 'trigger':
|
||||
Component = CanvasNodeDefault;
|
||||
break;
|
||||
|
||||
switch (node?.data.value.render.type) {
|
||||
// @TODO Add support for sticky notes here
|
||||
default:
|
||||
Component = CanvasNodeDefault;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,10 @@ describe('CanvasNodeToolbar', () => {
|
|||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
renderType: 'configuration',
|
||||
render: {
|
||||
type: 'default',
|
||||
options: { configuration: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, inject, useCssModule } from 'vue';
|
||||
import { CanvasNodeKey } from '@/constants';
|
||||
import { useCssModule } from 'vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [];
|
||||
toggle: [];
|
||||
run: [];
|
||||
}>();
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
const node = inject(CanvasNodeKey);
|
||||
|
||||
const data = computed(() => node?.data.value);
|
||||
const { renderOptions } = useCanvasNode();
|
||||
|
||||
// @TODO
|
||||
const workflowRunning = false;
|
||||
|
@ -20,8 +20,9 @@ const workflowRunning = false;
|
|||
// @TODO
|
||||
const nodeDisabledTitle = 'Test';
|
||||
|
||||
// @TODO
|
||||
function executeNode() {}
|
||||
function executeNode() {
|
||||
emit('run');
|
||||
}
|
||||
|
||||
function onToggleNode() {
|
||||
emit('toggle');
|
||||
|
@ -39,7 +40,7 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
|
|||
<div :class="$style.canvasNodeToolbar">
|
||||
<div :class="$style.canvasNodeToolbarItems">
|
||||
<N8nIconButton
|
||||
v-if="data?.renderType !== 'configuration'"
|
||||
v-if="!renderOptions.configuration"
|
||||
data-test-id="execute-node-button"
|
||||
type="tertiary"
|
||||
text
|
||||
|
@ -81,12 +82,17 @@ function openContextMenu(_e: MouseEvent, _type: string) {}
|
|||
|
||||
<style lang="scss" module>
|
||||
.canvasNodeToolbar {
|
||||
padding-bottom: var(--spacing-3xs);
|
||||
padding-bottom: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.canvasNodeToolbarItems {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color-canvas-background);
|
||||
|
||||
:global(.button) {
|
||||
--button-font-color: var(--color-text-light);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,107 +0,0 @@
|
|||
import CanvasNodeConfigurable from '@/components/canvas/elements/nodes/render-types/CanvasNodeConfigurable.vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||
import { setActivePinia } from 'pinia';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasNodeConfigurable);
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
});
|
||||
|
||||
describe('CanvasNodeConfigurable', () => {
|
||||
it('should render node correctly', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByText('Test Node')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('selected', () => {
|
||||
it('should apply selected class when node is selected', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
selected: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(getByText('Test Node').closest('.node')).toHaveClass('selected');
|
||||
});
|
||||
|
||||
it('should not apply selected class when node is not selected', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(getByText('Test Node').closest('.node')).not.toHaveClass('selected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('disabled', () => {
|
||||
it('should apply disabled class when node is disabled', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
disabled: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByText('Test Node').closest('.node')).toHaveClass('disabled');
|
||||
expect(getByText('(Deactivated)')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should not apply disabled class when node is enabled', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inputs', () => {
|
||||
it('should adjust width css variable based on the number of non-main inputs', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
inputs: [
|
||||
{ type: NodeConnectionType.Main },
|
||||
{ type: NodeConnectionType.AiTool },
|
||||
{ type: NodeConnectionType.AiDocument, required: true },
|
||||
{ type: NodeConnectionType.AiMemory, required: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const nodeElement = getByText('Test Node').closest('.node');
|
||||
expect(nodeElement).toHaveStyle({ '--configurable-node-input-count': '3' });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,134 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, useCssModule } from 'vue';
|
||||
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
||||
import { useNodeConnections } from '@/composables/useNodeConnections';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue';
|
||||
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
||||
const {
|
||||
label,
|
||||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
isDisabled,
|
||||
isSelected,
|
||||
hasPinnedData,
|
||||
hasRunData,
|
||||
hasIssues,
|
||||
} = useCanvasNode();
|
||||
const { nonMainInputs, requiredNonMainInputs } = useNodeConnections({
|
||||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
});
|
||||
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
[$style.node]: true,
|
||||
[$style.selected]: isSelected.value,
|
||||
[$style.disabled]: isDisabled.value,
|
||||
[$style.success]: hasRunData.value,
|
||||
[$style.error]: hasIssues.value,
|
||||
[$style.pinned]: hasPinnedData.value,
|
||||
};
|
||||
});
|
||||
|
||||
const styles = computed(() => {
|
||||
const stylesObject: {
|
||||
[key: string]: string | number;
|
||||
} = {};
|
||||
|
||||
if (requiredNonMainInputs.value.length > 0) {
|
||||
let spacerCount = 0;
|
||||
if (NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS) {
|
||||
const requiredNonMainInputsCount = requiredNonMainInputs.value.length;
|
||||
const optionalNonMainInputsCount = nonMainInputs.value.length - requiredNonMainInputsCount;
|
||||
spacerCount = requiredNonMainInputsCount > 0 && optionalNonMainInputsCount > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
stylesObject['--configurable-node-input-count'] = nonMainInputs.value.length + spacerCount;
|
||||
}
|
||||
|
||||
return stylesObject;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes" :style="styles" data-test-id="canvas-node-configurable">
|
||||
<slot />
|
||||
<CanvasNodeStatusIcons :class="$style.statusIcons" />
|
||||
<CanvasNodeDisabledStrikeThrough v-if="isDisabled" />
|
||||
<div :class="$style.label">
|
||||
{{ label }}
|
||||
<div v-if="isDisabled">({{ i18n.baseText('node.disabled') }})</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.node {
|
||||
--configurable-node-min-input-count: 4;
|
||||
--configurable-node-input-width: 65px;
|
||||
--canvas-node--height: 100px;
|
||||
--canvas-node--width: calc(
|
||||
max(var(--configurable-node-input-count, 5), var(--configurable-node-min-input-count)) *
|
||||
var(--configurable-node-input-width)
|
||||
);
|
||||
|
||||
width: var(--canvas-node--width);
|
||||
height: var(--canvas-node--height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--canvas-node--background, var(--color-node-background));
|
||||
border: 2px solid var(--canvas-node--border-color, var(--color-foreground-xdark));
|
||||
border-radius: var(--border-radius-large);
|
||||
|
||||
&.selected {
|
||||
box-shadow: 0 0 0 4px var(--color-canvas-selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* State classes
|
||||
* The reverse order defines the priority in case multiple states are active
|
||||
*/
|
||||
|
||||
&.error {
|
||||
border-color: var(--color-canvas-node-error-border-color, var(--color-danger));
|
||||
}
|
||||
|
||||
&.success {
|
||||
border-color: var(--color-canvas-node-success-border-color, var(--color-success));
|
||||
}
|
||||
|
||||
&.pinned {
|
||||
border-color: var(--color-canvas-node-pinned-border, var(--color-node-pinned-border));
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
border-color: var(--color-canvas-node-disabled-border, var(--color-foreground-base));
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
top: 100%;
|
||||
font-size: var(--font-size-m);
|
||||
text-align: center;
|
||||
margin-left: var(--spacing-s);
|
||||
max-width: calc(
|
||||
var(--node-width) - var(--configurable-node-icon-offset) - var(--configurable-node-icon-size) -
|
||||
2 * var(--spacing-s)
|
||||
);
|
||||
}
|
||||
|
||||
.statusIcons {
|
||||
position: absolute;
|
||||
top: calc(var(--canvas-node--height) - 24px);
|
||||
right: var(--spacing-xs);
|
||||
}
|
||||
</style>
|
|
@ -1,80 +0,0 @@
|
|||
import CanvasNodeConfiguration from '@/components/canvas/elements/nodes/render-types/CanvasNodeConfiguration.vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { setActivePinia } from 'pinia';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasNodeConfiguration);
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createTestingPinia();
|
||||
setActivePinia(pinia);
|
||||
});
|
||||
|
||||
describe('CanvasNodeConfiguration', () => {
|
||||
it('should render node correctly', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByText('Test Node')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('selected', () => {
|
||||
it('should apply selected class when node is selected', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({ selected: true }),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(getByText('Test Node').closest('.node')).toHaveClass('selected');
|
||||
});
|
||||
|
||||
it('should not apply selected class when node is not selected', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(getByText('Test Node').closest('.node')).not.toHaveClass('selected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('disabled', () => {
|
||||
it('should apply disabled class when node is disabled', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
disabled: true,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByText('Test Node').closest('.node')).toHaveClass('disabled');
|
||||
expect(getByText('(Deactivated)')).toBeVisible();
|
||||
});
|
||||
|
||||
it('should not apply disabled class when node is enabled', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,79 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { computed, useCssModule } from 'vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
||||
const { label, isDisabled, isSelected, hasIssues } = useCanvasNode();
|
||||
|
||||
const classes = computed(() => {
|
||||
return {
|
||||
[$style.node]: true,
|
||||
[$style.selected]: isSelected.value,
|
||||
[$style.disabled]: isDisabled.value,
|
||||
[$style.error]: hasIssues.value,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes" data-test-id="canvas-node-configuration">
|
||||
<slot />
|
||||
<CanvasNodeStatusIcons :class="$style.statusIcons" />
|
||||
<div v-if="label" :class="$style.label">
|
||||
{{ label }}
|
||||
<div v-if="isDisabled">({{ i18n.baseText('node.disabled') }})</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.node {
|
||||
--canvas-node--width: 75px;
|
||||
--canvas-node--height: 75px;
|
||||
|
||||
width: var(--canvas-node--width);
|
||||
height: var(--canvas-node--height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--canvas-node--background, var(--node-type-supplemental-background));
|
||||
border: 2px solid var(--canvas-node--border-color, var(--color-foreground-dark));
|
||||
border-radius: 50%;
|
||||
|
||||
/**
|
||||
* State classes
|
||||
* The reverse order defines the priority in case multiple states are active
|
||||
*/
|
||||
|
||||
&.selected {
|
||||
box-shadow: 0 0 0 4px var(--color-canvas-selected);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--color-canvas-node-error-border-color, var(--color-danger));
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
border-color: var(--color-canvas-node-disabled-border, var(--color-foreground-base));
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
top: 100%;
|
||||
position: absolute;
|
||||
font-size: var(--font-size-m);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
margin-top: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.statusIcons {
|
||||
position: absolute;
|
||||
top: calc(var(--canvas-node--height) - 24px);
|
||||
}
|
||||
</style>
|
|
@ -14,7 +14,7 @@ beforeEach(() => {
|
|||
|
||||
describe('CanvasNodeDefault', () => {
|
||||
it('should render node correctly', () => {
|
||||
const { getByText } = renderComponent({
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide(),
|
||||
|
@ -22,7 +22,7 @@ describe('CanvasNodeDefault', () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(getByText('Test Node')).toBeInTheDocument();
|
||||
expect(getByTestId('canvas-default-node')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('outputs', () => {
|
||||
|
@ -32,7 +32,7 @@ describe('CanvasNodeDefault', () => {
|
|||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
outputs: [{ type: NodeConnectionType.Main }],
|
||||
outputs: [{ type: NodeConnectionType.Main, index: 0 }],
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
@ -40,7 +40,7 @@ describe('CanvasNodeDefault', () => {
|
|||
});
|
||||
|
||||
const nodeElement = getByText('Test Node').closest('.node');
|
||||
expect(nodeElement).toHaveStyle({ '--node-main-output-count': '1' }); // height calculation based on the number of outputs
|
||||
expect(nodeElement).toHaveStyle({ '--canvas-node--main-output-count': '1' }); // height calculation based on the number of outputs
|
||||
});
|
||||
|
||||
it('should adjust height css variable based on the number of outputs (multiple outputs)', () => {
|
||||
|
@ -50,9 +50,9 @@ describe('CanvasNodeDefault', () => {
|
|||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
outputs: [
|
||||
{ type: NodeConnectionType.Main },
|
||||
{ type: NodeConnectionType.Main },
|
||||
{ type: NodeConnectionType.Main },
|
||||
{ type: NodeConnectionType.Main, index: 0 },
|
||||
{ type: NodeConnectionType.Main, index: 0 },
|
||||
{ type: NodeConnectionType.Main, index: 0 },
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
@ -61,7 +61,7 @@ describe('CanvasNodeDefault', () => {
|
|||
});
|
||||
|
||||
const nodeElement = getByText('Test Node').closest('.node');
|
||||
expect(nodeElement).toHaveStyle({ '--node-main-output-count': '3' }); // height calculation based on the number of outputs
|
||||
expect(nodeElement).toHaveStyle({ '--canvas-node--main-output-count': '3' }); // height calculation based on the number of outputs
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -118,4 +118,108 @@ describe('CanvasNodeDefault', () => {
|
|||
expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('running', () => {
|
||||
it('should apply running class when node is running', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({ data: { execution: { running: true } } }),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(getByText('Test Node').closest('.node')).toHaveClass('running');
|
||||
});
|
||||
});
|
||||
|
||||
describe('configurable', () => {
|
||||
it('should render configurable node correctly', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
render: {
|
||||
type: 'default',
|
||||
options: { configurable: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('canvas-configurable-node')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('inputs', () => {
|
||||
it('should adjust width css variable based on the number of non-main inputs', () => {
|
||||
const { getByText } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
inputs: [
|
||||
{ type: NodeConnectionType.Main, index: 0 },
|
||||
{ type: NodeConnectionType.AiTool, index: 0 },
|
||||
{ type: NodeConnectionType.AiDocument, index: 0, required: true },
|
||||
{ type: NodeConnectionType.AiMemory, index: 0, required: true },
|
||||
],
|
||||
render: {
|
||||
type: 'default',
|
||||
options: {
|
||||
configurable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const nodeElement = getByText('Test Node').closest('.node');
|
||||
expect(nodeElement).toHaveStyle({ '--configurable-node--input-count': '3' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('configuration', () => {
|
||||
it('should render configuration node correctly', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
render: {
|
||||
type: 'default',
|
||||
options: { configuration: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('canvas-configuration-node')).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render configurable configuration node correctly', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: {
|
||||
...createCanvasNodeProvide({
|
||||
data: {
|
||||
render: {
|
||||
type: 'default',
|
||||
options: { configurable: true, configuration: true },
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('canvas-configurable-node')).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useI18n } from '@/composables/useI18n';
|
|||
import CanvasNodeDisabledStrikeThrough from './parts/CanvasNodeDisabledStrikeThrough.vue';
|
||||
import CanvasNodeStatusIcons from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeStatusIcons.vue';
|
||||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import { NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS } from '@/constants';
|
||||
|
||||
const $style = useCssModule();
|
||||
const i18n = useI18n();
|
||||
|
@ -17,10 +18,12 @@ const {
|
|||
isDisabled,
|
||||
isSelected,
|
||||
hasPinnedData,
|
||||
executionRunning,
|
||||
hasRunData,
|
||||
hasIssues,
|
||||
renderOptions,
|
||||
} = useCanvasNode();
|
||||
const { mainOutputs } = useNodeConnections({
|
||||
const { mainOutputs, nonMainInputs, requiredNonMainInputs } = useNodeConnections({
|
||||
inputs,
|
||||
outputs,
|
||||
connections,
|
||||
|
@ -34,18 +37,48 @@ const classes = computed(() => {
|
|||
[$style.success]: hasRunData.value,
|
||||
[$style.error]: hasIssues.value,
|
||||
[$style.pinned]: hasPinnedData.value,
|
||||
[$style.running]: executionRunning.value,
|
||||
[$style.configurable]: renderOptions.value.configurable,
|
||||
[$style.configuration]: renderOptions.value.configuration,
|
||||
[$style.trigger]: renderOptions.value.trigger,
|
||||
};
|
||||
});
|
||||
|
||||
const styles = computed(() => {
|
||||
return {
|
||||
'--node-main-output-count': mainOutputs.value.length,
|
||||
};
|
||||
const stylesObject: Record<string, string | number> = {};
|
||||
|
||||
if (renderOptions.value.configurable && requiredNonMainInputs.value.length > 0) {
|
||||
let spacerCount = 0;
|
||||
if (NODE_INSERT_SPACER_BETWEEN_INPUT_GROUPS) {
|
||||
const requiredNonMainInputsCount = requiredNonMainInputs.value.length;
|
||||
const optionalNonMainInputsCount = nonMainInputs.value.length - requiredNonMainInputsCount;
|
||||
spacerCount = requiredNonMainInputsCount > 0 && optionalNonMainInputsCount > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
stylesObject['--configurable-node--input-count'] = nonMainInputs.value.length + spacerCount;
|
||||
}
|
||||
|
||||
stylesObject['--canvas-node--main-output-count'] = mainOutputs.value.length;
|
||||
|
||||
return stylesObject;
|
||||
});
|
||||
|
||||
const dataTestId = computed(() => {
|
||||
let type = 'default';
|
||||
if (renderOptions.value.configurable) {
|
||||
type = 'configurable';
|
||||
} else if (renderOptions.value.configuration) {
|
||||
type = 'configuration';
|
||||
} else if (renderOptions.value.trigger) {
|
||||
type = 'trigger';
|
||||
}
|
||||
|
||||
return `canvas-${type}-node`;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes" :style="styles" data-test-id="canvas-node-default">
|
||||
<div :class="classes" :style="styles" :data-test-id="dataTestId">
|
||||
<slot />
|
||||
<CanvasNodeStatusIcons :class="$style.statusIcons" />
|
||||
<CanvasNodeDisabledStrikeThrough v-if="isDisabled" />
|
||||
|
@ -58,8 +91,12 @@ const styles = computed(() => {
|
|||
|
||||
<style lang="scss" module>
|
||||
.node {
|
||||
--canvas-node--height: calc(100px + max(0, var(--node-main-output-count, 1) - 4) * 50px);
|
||||
--canvas-node--height: calc(100px + max(0, var(--canvas-node--main-output-count, 1) - 4) * 50px);
|
||||
--canvas-node--width: 100px;
|
||||
--configurable-node--min-input-count: 4;
|
||||
--configurable-node--input-width: 65px;
|
||||
--configurable-node--icon-offset: 40px;
|
||||
--configurable-node--icon-size: 30px;
|
||||
|
||||
height: var(--canvas-node--height);
|
||||
width: var(--canvas-node--width);
|
||||
|
@ -70,6 +107,44 @@ const styles = computed(() => {
|
|||
border: 2px solid var(--canvas-node--border-color, var(--color-foreground-xdark));
|
||||
border-radius: var(--border-radius-large);
|
||||
|
||||
/**
|
||||
* Node types
|
||||
*/
|
||||
|
||||
&.configuration {
|
||||
--canvas-node--width: 75px;
|
||||
--canvas-node--height: 75px;
|
||||
|
||||
background: var(--canvas-node--background, var(--node-type-supplemental-background));
|
||||
border: 2px solid var(--canvas-node--border-color, var(--color-foreground-dark));
|
||||
border-radius: 50px;
|
||||
|
||||
.statusIcons {
|
||||
right: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&.configurable {
|
||||
--canvas-node--height: 100px;
|
||||
--canvas-node--width: calc(
|
||||
max(var(--configurable-node--input-count, 5), var(--configurable-node--min-input-count)) *
|
||||
var(--configurable-node--input-width)
|
||||
);
|
||||
|
||||
.label {
|
||||
top: unset;
|
||||
position: relative;
|
||||
margin-left: var(--spacing-s);
|
||||
width: auto;
|
||||
min-width: unset;
|
||||
max-width: calc(
|
||||
var(--canvas-node--width) - var(--configurable-node--icon-offset) - var(
|
||||
--configurable-node--icon-size
|
||||
) - 2 * var(--spacing-s)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State classes
|
||||
* The reverse order defines the priority in case multiple states are active
|
||||
|
@ -94,6 +169,11 @@ const styles = computed(() => {
|
|||
&.disabled {
|
||||
border-color: var(--color-canvas-node-disabled-border-color, var(--color-foreground-base));
|
||||
}
|
||||
|
||||
&.running {
|
||||
background-color: var(--color-node-executing-background);
|
||||
border-color: var(--color-canvas-node-running-border-color, var(--color-node-running-border));
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
|
@ -108,7 +188,7 @@ const styles = computed(() => {
|
|||
|
||||
.statusIcons {
|
||||
position: absolute;
|
||||
top: calc(var(--canvas-node--height) - 24px);
|
||||
right: var(--spacing-xs);
|
||||
bottom: var(--spacing-2xs);
|
||||
right: var(--spacing-2xs);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`CanvasNodeDefault > configurable > should render configurable node correctly 1`] = `
|
||||
<div
|
||||
class="node configurable"
|
||||
data-test-id="canvas-configurable-node"
|
||||
style="--canvas-node--main-output-count: 0;"
|
||||
>
|
||||
|
||||
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="label"
|
||||
>
|
||||
Test Node
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CanvasNodeDefault > configuration > should render configurable configuration node correctly 1`] = `
|
||||
<div
|
||||
class="node configurable configuration"
|
||||
data-test-id="canvas-configurable-node"
|
||||
style="--canvas-node--main-output-count: 0;"
|
||||
>
|
||||
|
||||
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="label"
|
||||
>
|
||||
Test Node
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CanvasNodeDefault > configuration > should render configuration node correctly 1`] = `
|
||||
<div
|
||||
class="node configuration"
|
||||
data-test-id="canvas-configuration-node"
|
||||
style="--canvas-node--main-output-count: 0;"
|
||||
>
|
||||
|
||||
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="label"
|
||||
>
|
||||
Test Node
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`CanvasNodeDefault > should render node correctly 1`] = `
|
||||
<div
|
||||
class="node"
|
||||
data-test-id="canvas-default-node"
|
||||
style="--canvas-node--main-output-count: 0;"
|
||||
>
|
||||
|
||||
|
||||
<!--v-if-->
|
||||
<!--v-if-->
|
||||
<div
|
||||
class="label"
|
||||
>
|
||||
Test Node
|
||||
<!--v-if-->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,40 @@
|
|||
import CanvasNodeStatusIcons from './CanvasNodeStatusIcons.vue';
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createCanvasNodeProvide } from '@/__tests__/data';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
|
||||
const renderComponent = createComponentRenderer(CanvasNodeStatusIcons, {
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
|
||||
describe('CanvasNodeStatusIcons', () => {
|
||||
it('should render correctly for a pinned node', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: createCanvasNodeProvide({ data: { pinnedData: { count: 5, visible: true } } }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('canvas-node-status-pinned')).toHaveTextContent('5');
|
||||
});
|
||||
|
||||
it('should render correctly for a running node', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: createCanvasNodeProvide({ data: { execution: { running: true } } }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('canvas-node-status-running')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render correctly for a node that ran successfully', () => {
|
||||
const { getByTestId } = renderComponent({
|
||||
global: {
|
||||
provide: createCanvasNodeProvide({ data: { runData: { count: 15, visible: true } } }),
|
||||
},
|
||||
});
|
||||
|
||||
expect(getByTestId('canvas-node-status-success')).toHaveTextContent('15');
|
||||
});
|
||||
});
|
|
@ -13,6 +13,7 @@ const {
|
|||
hasIssues,
|
||||
executionStatus,
|
||||
executionWaiting,
|
||||
executionRunning,
|
||||
hasRunData,
|
||||
runDataCount,
|
||||
} = useCanvasNode();
|
||||
|
@ -21,46 +22,62 @@ const hideNodeIssues = computed(() => false); // @TODO Implement this
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.canvasNodeStatusIcons">
|
||||
<div v-if="hasIssues && !hideNodeIssues" :class="$style.issues" data-test-id="node-issues">
|
||||
<N8nTooltip :show-after="500" placement="bottom">
|
||||
<template #content>
|
||||
<TitledList :title="`${$locale.baseText('node.issues')}:`" :items="issues" />
|
||||
</template>
|
||||
<FontAwesomeIcon icon="exclamation-triangle" />
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<div v-else-if="executionWaiting || executionStatus === 'waiting'" class="waiting">
|
||||
<N8nTooltip placement="bottom">
|
||||
<template #content>
|
||||
<div v-text="executionWaiting"></div>
|
||||
</template>
|
||||
<FontAwesomeIcon icon="clock" />
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<span
|
||||
v-else-if="hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value"
|
||||
:class="$style.pinnedData"
|
||||
>
|
||||
<FontAwesomeIcon icon="thumbtack" />
|
||||
<span v-if="pinnedDataCount > 1" class="items-count"> {{ pinnedDataCount }}</span>
|
||||
</span>
|
||||
<span v-else-if="executionStatus === 'unknown'">
|
||||
<!-- Do nothing, unknown means the node never executed -->
|
||||
</span>
|
||||
<span v-else-if="hasRunData" :class="$style.runData">
|
||||
<FontAwesomeIcon icon="check" />
|
||||
<span v-if="runDataCount > 1" :class="$style.itemsCount"> {{ runDataCount }}</span>
|
||||
</span>
|
||||
<div
|
||||
v-if="hasIssues && !hideNodeIssues"
|
||||
:class="[$style.status, $style.issues]"
|
||||
data-test-id="node-issues"
|
||||
>
|
||||
<N8nTooltip :show-after="500" placement="bottom">
|
||||
<template #content>
|
||||
<TitledList :title="`${$locale.baseText('node.issues')}:`" :items="issues" />
|
||||
</template>
|
||||
<FontAwesomeIcon icon="exclamation-triangle" />
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="executionWaiting || executionStatus === 'waiting'"
|
||||
:class="[$style.status, $style.waiting]"
|
||||
>
|
||||
<N8nTooltip placement="bottom">
|
||||
<template #content>
|
||||
<div v-text="executionWaiting"></div>
|
||||
</template>
|
||||
<FontAwesomeIcon icon="clock" />
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="hasPinnedData && !nodeHelpers.isProductionExecutionPreview.value"
|
||||
data-test-id="canvas-node-status-pinned"
|
||||
:class="[$style.status, $style.pinnedData]"
|
||||
>
|
||||
<FontAwesomeIcon icon="thumbtack" />
|
||||
<span v-if="pinnedDataCount > 1" :class="$style.count"> {{ pinnedDataCount }}</span>
|
||||
</div>
|
||||
<div v-else-if="executionStatus === 'unknown'">
|
||||
<!-- Do nothing, unknown means the node never executed -->
|
||||
</div>
|
||||
<div
|
||||
v-else-if="executionRunning || executionStatus === 'running'"
|
||||
data-test-id="canvas-node-status-running"
|
||||
:class="[$style.status, $style.running]"
|
||||
>
|
||||
<FontAwesomeIcon icon="sync-alt" spin />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="hasRunData"
|
||||
data-test-id="canvas-node-status-success"
|
||||
:class="[$style.status, $style.runData]"
|
||||
>
|
||||
<FontAwesomeIcon icon="check" />
|
||||
<span v-if="runDataCount > 1" :class="$style.count"> {{ runDataCount }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.canvasNodeStatusIcons {
|
||||
.status {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-5xs);
|
||||
}
|
||||
|
||||
.runData {
|
||||
|
@ -72,12 +89,16 @@ const hideNodeIssues = computed(() => false); // @TODO Implement this
|
|||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.running {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.issues {
|
||||
color: var(--color-danger);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.itemsCount {
|
||||
.count {
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import type { Connection } from '@vue-flow/core';
|
||||
import type { IConnection } from 'n8n-workflow';
|
||||
import type { IConnection, Workflow } from 'n8n-workflow';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import { useCanvasOperations } from '@/composables/useCanvasOperations';
|
||||
import type { CanvasElement } from '@/types';
|
||||
|
@ -65,7 +65,7 @@ describe('useCanvasOperations', () => {
|
|||
usedCredentials: [],
|
||||
});
|
||||
workflowsStore.workflowsById[workflowId] = workflow;
|
||||
await workflowHelpers.initState(workflow, true);
|
||||
await workflowHelpers.initState(workflow);
|
||||
|
||||
canvasOperations = useCanvasOperations({ router, lastClickPosition });
|
||||
});
|
||||
|
@ -248,8 +248,8 @@ describe('useCanvasOperations', () => {
|
|||
it('should add nodes at current position when position is not specified', async () => {
|
||||
const nodeTypeName = 'type';
|
||||
const nodes = [
|
||||
mockNode({ name: 'Node 1', type: nodeTypeName, position: [40, 40] }),
|
||||
mockNode({ name: 'Node 2', type: nodeTypeName, position: [100, 240] }),
|
||||
mockNode({ name: 'Node 1', type: nodeTypeName, position: [120, 120] }),
|
||||
mockNode({ name: 'Node 2', type: nodeTypeName, position: [180, 320] }),
|
||||
];
|
||||
const workflowStoreAddNodeSpy = vi.spyOn(workflowsStore, 'addNode');
|
||||
|
||||
|
@ -292,9 +292,16 @@ describe('useCanvasOperations', () => {
|
|||
}),
|
||||
]);
|
||||
|
||||
canvasOperations.editableWorkflowObject.value.getParentNodesByDepth = vi
|
||||
.fn()
|
||||
.mockReturnValue(nodes.map((node) => node.name));
|
||||
vi.spyOn(workflowsStore, 'getCurrentWorkflow').mockImplementation(() =>
|
||||
mock<Workflow>({
|
||||
getParentNodesByDepth: () =>
|
||||
nodes.map((node) => ({
|
||||
name: node.name,
|
||||
depth: 0,
|
||||
indicies: [],
|
||||
})),
|
||||
}),
|
||||
);
|
||||
|
||||
await canvasOperations.addNodes(nodes, {});
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
} from '@/__tests__/mocks';
|
||||
import { MANUAL_TRIGGER_NODE_TYPE, SET_NODE_TYPE } from '@/constants';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useWorkflowsStore } from '../stores/workflows.store';
|
||||
|
||||
beforeEach(() => {
|
||||
const pinia = createPinia();
|
||||
|
@ -80,6 +81,7 @@ describe('useCanvasMapping', () => {
|
|||
disabled: false,
|
||||
execution: {
|
||||
status: 'new',
|
||||
running: false,
|
||||
waiting: undefined,
|
||||
},
|
||||
issues: {
|
||||
|
@ -112,7 +114,14 @@ describe('useCanvasMapping', () => {
|
|||
input: {},
|
||||
output: {},
|
||||
},
|
||||
renderType: 'trigger',
|
||||
render: {
|
||||
type: 'default',
|
||||
options: {
|
||||
configurable: false,
|
||||
configuration: false,
|
||||
trigger: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
@ -137,6 +146,27 @@ describe('useCanvasMapping', () => {
|
|||
expect(elements.value[0]?.data?.disabled).toEqual(true);
|
||||
});
|
||||
|
||||
it('should handle execution state', () => {
|
||||
const manualTriggerNode = mockNode({
|
||||
name: 'Manual Trigger',
|
||||
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||
disabled: true,
|
||||
});
|
||||
const workflow = mock<IWorkflowDb>({
|
||||
nodes: [manualTriggerNode],
|
||||
});
|
||||
const workflowObject = createTestWorkflowObject(workflow);
|
||||
|
||||
useWorkflowsStore().addExecutingNode(manualTriggerNode.name);
|
||||
|
||||
const { elements } = useCanvasMapping({
|
||||
workflow: ref(workflow),
|
||||
workflowObject: ref(workflowObject) as Ref<Workflow>,
|
||||
});
|
||||
|
||||
expect(elements.value[0]?.data?.execution.running).toEqual(true);
|
||||
});
|
||||
|
||||
it('should handle input and output connections', () => {
|
||||
const [manualTriggerNode, setNode] = mockNodes.slice(0, 2);
|
||||
const workflow = mock<IWorkflowDb>({
|
||||
|
@ -217,6 +247,7 @@ describe('useCanvasMapping', () => {
|
|||
target: setNode.id,
|
||||
targetHandle: `inputs/${NodeConnectionType.Main}/0`,
|
||||
type: 'canvas-edge',
|
||||
animated: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -264,6 +295,7 @@ describe('useCanvasMapping', () => {
|
|||
target: setNode.id,
|
||||
targetHandle: `inputs/${NodeConnectionType.AiTool}/0`,
|
||||
type: 'canvas-edge',
|
||||
animated: false,
|
||||
},
|
||||
{
|
||||
data: {
|
||||
|
@ -285,6 +317,7 @@ describe('useCanvasMapping', () => {
|
|||
target: setNode.id,
|
||||
targetHandle: `inputs/${NodeConnectionType.AiDocument}/1`,
|
||||
type: 'canvas-edge',
|
||||
animated: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -44,21 +44,18 @@ export function useCanvasMapping({
|
|||
|
||||
const renderTypeByNodeType = computed(
|
||||
() =>
|
||||
workflow.value.nodes.reduce<Record<string, CanvasElementData['renderType']>>((acc, node) => {
|
||||
let renderType: CanvasElementData['renderType'] = 'default';
|
||||
switch (true) {
|
||||
case nodeTypesStore.isTriggerNode(node.type):
|
||||
renderType = 'trigger';
|
||||
break;
|
||||
case nodeTypesStore.isConfigNode(workflowObject.value, node, node.type):
|
||||
renderType = 'configuration';
|
||||
break;
|
||||
case nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type):
|
||||
renderType = 'configurable';
|
||||
break;
|
||||
}
|
||||
workflow.value.nodes.reduce<Record<string, CanvasElementData['render']>>((acc, node) => {
|
||||
// @TODO Add support for sticky notes here
|
||||
|
||||
acc[node.type] = {
|
||||
type: 'default',
|
||||
options: {
|
||||
trigger: nodeTypesStore.isTriggerNode(node.type),
|
||||
configuration: nodeTypesStore.isConfigNode(workflowObject.value, node, node.type),
|
||||
configurable: nodeTypesStore.isConfigurableNode(workflowObject.value, node, node.type),
|
||||
},
|
||||
};
|
||||
|
||||
acc[node.type] = renderType;
|
||||
return acc;
|
||||
}, {}) ?? {},
|
||||
);
|
||||
|
@ -110,6 +107,13 @@ export function useCanvasMapping({
|
|||
}, {}),
|
||||
);
|
||||
|
||||
const nodeExecutionRunningById = computed(() =>
|
||||
workflow.value.nodes.reduce<Record<string, boolean>>((acc, node) => {
|
||||
acc[node.id] = workflowsStore.isNodeExecuting(node.name);
|
||||
return acc;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
const nodeExecutionStatusById = computed(() =>
|
||||
workflow.value.nodes.reduce<Record<string, ExecutionStatus>>((acc, node) => {
|
||||
acc[node.id] =
|
||||
|
@ -221,12 +225,13 @@ export function useCanvasMapping({
|
|||
execution: {
|
||||
status: nodeExecutionStatusById.value[node.id],
|
||||
waiting: nodeExecutionWaitingById.value[node.id],
|
||||
running: nodeExecutionRunningById.value[node.id],
|
||||
},
|
||||
runData: {
|
||||
count: nodeExecutionRunDataById.value[node.id]?.length ?? 0,
|
||||
visible: !!nodeExecutionRunDataById.value[node.id],
|
||||
},
|
||||
renderType: renderTypeByNodeType.value[node.type] ?? 'default',
|
||||
render: renderTypeByNodeType.value[node.type] ?? { type: 'default', options: {} },
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -255,6 +260,7 @@ export function useCanvasMapping({
|
|||
data,
|
||||
type,
|
||||
label,
|
||||
animated: data.status === 'running',
|
||||
};
|
||||
});
|
||||
});
|
||||
|
@ -266,7 +272,12 @@ export function useCanvasMapping({
|
|||
|
||||
let status: CanvasConnectionData['status'];
|
||||
if (fromNode) {
|
||||
if (nodePinnedDataById.value[fromNode.id] && nodeExecutionRunDataById.value[fromNode.id]) {
|
||||
if (nodeExecutionRunningById.value[fromNode.id]) {
|
||||
status = 'running';
|
||||
} else if (
|
||||
nodePinnedDataById.value[fromNode.id] &&
|
||||
nodeExecutionRunDataById.value[fromNode.id]
|
||||
) {
|
||||
status = 'pinned';
|
||||
} else if (nodeHasIssuesById.value[fromNode.id]) {
|
||||
status = 'error';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useCanvasNode } from '@/composables/useCanvasNode';
|
||||
import { inject, ref } from 'vue';
|
||||
import type { CanvasNodeInjectionData } from '../types';
|
||||
|
||||
vi.mock('vue', async () => {
|
||||
const actual = await vi.importActual('vue');
|
||||
|
@ -27,38 +28,46 @@ describe('useCanvasNode', () => {
|
|||
expect(result.hasIssues.value).toBe(false);
|
||||
expect(result.executionStatus.value).toBeUndefined();
|
||||
expect(result.executionWaiting.value).toBeUndefined();
|
||||
expect(result.executionRunning.value).toBe(false);
|
||||
expect(result.renderOptions.value).toEqual({});
|
||||
});
|
||||
|
||||
it('should return node data when node is provided', () => {
|
||||
const node = {
|
||||
data: {
|
||||
value: {
|
||||
id: 'node1',
|
||||
type: 'nodeType1',
|
||||
typeVersion: 1,
|
||||
disabled: true,
|
||||
inputs: ['input1'],
|
||||
outputs: ['output1'],
|
||||
connections: { input: { '0': ['node2'] }, output: {} },
|
||||
issues: { items: ['issue1'], visible: true },
|
||||
execution: { status: 'running', waiting: false },
|
||||
runData: { count: 1, visible: true },
|
||||
pinnedData: { count: 1, visible: true },
|
||||
renderType: 'default',
|
||||
data: ref({
|
||||
id: 'node1',
|
||||
type: 'nodeType1',
|
||||
typeVersion: 1,
|
||||
disabled: true,
|
||||
inputs: [{ type: 'main', index: 0 }],
|
||||
outputs: [{ type: 'main', index: 0 }],
|
||||
connections: { input: { '0': [] }, output: {} },
|
||||
issues: { items: ['issue1'], visible: true },
|
||||
execution: { status: 'running', waiting: 'waiting', running: true },
|
||||
runData: { count: 1, visible: true },
|
||||
pinnedData: { count: 1, visible: true },
|
||||
render: {
|
||||
type: 'default',
|
||||
options: {
|
||||
configurable: false,
|
||||
configuration: false,
|
||||
trigger: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
id: ref('1'),
|
||||
label: ref('Node 1'),
|
||||
selected: ref(true),
|
||||
};
|
||||
} satisfies Partial<CanvasNodeInjectionData>;
|
||||
|
||||
vi.mocked(inject).mockReturnValue(node);
|
||||
|
||||
const result = useCanvasNode();
|
||||
|
||||
expect(result.label.value).toBe('Node 1');
|
||||
expect(result.inputs.value).toEqual(['input1']);
|
||||
expect(result.outputs.value).toEqual(['output1']);
|
||||
expect(result.connections.value).toEqual({ input: { '0': ['node2'] }, output: {} });
|
||||
expect(result.inputs.value).toEqual([{ type: 'main', index: 0 }]);
|
||||
expect(result.outputs.value).toEqual([{ type: 'main', index: 0 }]);
|
||||
expect(result.connections.value).toEqual({ input: { '0': [] }, output: {} });
|
||||
expect(result.isDisabled.value).toBe(true);
|
||||
expect(result.isSelected.value).toBe(true);
|
||||
expect(result.pinnedDataCount.value).toBe(1);
|
||||
|
@ -68,6 +77,8 @@ describe('useCanvasNode', () => {
|
|||
expect(result.issues.value).toEqual(['issue1']);
|
||||
expect(result.hasIssues.value).toBe(true);
|
||||
expect(result.executionStatus.value).toBe('running');
|
||||
expect(result.executionWaiting.value).toBe(false);
|
||||
expect(result.executionWaiting.value).toBe('waiting');
|
||||
expect(result.executionRunning.value).toBe(true);
|
||||
expect(result.renderOptions.value).toBe(node.data.value.render.options);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,9 +21,14 @@ export function useCanvasNode() {
|
|||
connections: { input: {}, output: {} },
|
||||
issues: { items: [], visible: false },
|
||||
pinnedData: { count: 0, visible: false },
|
||||
execution: {},
|
||||
execution: {
|
||||
running: false,
|
||||
},
|
||||
runData: { count: 0, visible: false },
|
||||
renderType: 'default',
|
||||
render: {
|
||||
type: 'default',
|
||||
options: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -45,10 +50,13 @@ export function useCanvasNode() {
|
|||
|
||||
const executionStatus = computed(() => data.value.execution.status);
|
||||
const executionWaiting = computed(() => data.value.execution.waiting);
|
||||
const executionRunning = computed(() => data.value.execution.running);
|
||||
|
||||
const runDataCount = computed(() => data.value.runData.count);
|
||||
const hasRunData = computed(() => data.value.runData.visible);
|
||||
|
||||
const renderOptions = computed(() => data.value.render.options);
|
||||
|
||||
return {
|
||||
node,
|
||||
label,
|
||||
|
@ -65,5 +73,7 @@ export function useCanvasNode() {
|
|||
hasIssues,
|
||||
executionStatus,
|
||||
executionWaiting,
|
||||
executionRunning,
|
||||
renderOptions,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,13 +11,19 @@ import type {
|
|||
INodeUpdatePropertiesInformation,
|
||||
XYPosition,
|
||||
} from '@/Interface';
|
||||
import { QUICKSTART_NOTE_NAME, STICKY_NODE_TYPE } from '@/constants';
|
||||
import {
|
||||
FORM_TRIGGER_NODE_TYPE,
|
||||
QUICKSTART_NOTE_NAME,
|
||||
STICKY_NODE_TYPE,
|
||||
WEBHOOK_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useHistoryStore } from '@/stores/history.store';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import {
|
||||
AddNodeCommand,
|
||||
MoveNodeCommand,
|
||||
RemoveConnectionCommand,
|
||||
RemoveNodeCommand,
|
||||
|
@ -53,10 +59,8 @@ import type { useRouter } from 'vue-router';
|
|||
import { useCanvasStore } from '@/stores/canvas.store';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
|
||||
type AddNodeData = {
|
||||
name?: string;
|
||||
type AddNodeData = Partial<INodeUi> & {
|
||||
type: string;
|
||||
position?: XYPosition;
|
||||
};
|
||||
|
||||
type AddNodeOptions = {
|
||||
|
@ -266,13 +270,12 @@ export function useCanvasOperations({
|
|||
) {
|
||||
let currentPosition = position;
|
||||
let lastAddedNode: INodeUi | undefined;
|
||||
for (const { type, name, position: nodePosition, isAutoAdd, openDetail } of nodes) {
|
||||
for (const { isAutoAdd, openDetail, ...nodeData } of nodes) {
|
||||
try {
|
||||
await createNode(
|
||||
{
|
||||
name,
|
||||
type,
|
||||
position: nodePosition ?? currentPosition,
|
||||
...nodeData,
|
||||
position: nodeData.position ?? currentPosition,
|
||||
},
|
||||
{
|
||||
dragAndDrop,
|
||||
|
@ -328,14 +331,16 @@ export function useCanvasOperations({
|
|||
|
||||
workflowsStore.addNode(newNodeData);
|
||||
|
||||
// @TODO Figure out why this is needed and if we can do better...
|
||||
// this.matchCredentials(node);
|
||||
nodeHelpers.matchCredentials(newNodeData);
|
||||
|
||||
const lastSelectedNode = uiStore.getLastSelectedNode;
|
||||
const lastSelectedNodeOutputIndex = uiStore.lastSelectedNodeOutputIndex;
|
||||
const lastSelectedNodeEndpointUuid = uiStore.lastSelectedNodeEndpointUuid;
|
||||
|
||||
historyStore.startRecordingUndo();
|
||||
if (options.trackHistory) {
|
||||
historyStore.pushCommandToUndo(new AddNodeCommand(newNodeData));
|
||||
}
|
||||
|
||||
const outputIndex = lastSelectedNodeOutputIndex ?? 0;
|
||||
const targetEndpoint = lastSelectedNodeEndpointUuid ?? '';
|
||||
|
@ -399,12 +404,14 @@ export function useCanvasOperations({
|
|||
}
|
||||
|
||||
const newNodeData: INodeUi = {
|
||||
id: uuid(),
|
||||
...node,
|
||||
id: node.id ?? uuid(),
|
||||
name: node.name ?? (nodeTypeDescription.defaults.name as string),
|
||||
type: nodeTypeDescription.name,
|
||||
typeVersion: nodeVersion,
|
||||
position: node.position ?? [0, 0],
|
||||
parameters: {},
|
||||
disabled: node.disabled ?? false,
|
||||
parameters: node.parameters ?? {},
|
||||
};
|
||||
|
||||
await loadNodeTypesProperties([{ name: newNodeData.type, version: newNodeData.typeVersion }]);
|
||||
|
@ -664,6 +671,14 @@ export function useCanvasOperations({
|
|||
newNodeData.webhookId = uuid();
|
||||
}
|
||||
|
||||
// if it's a webhook and the path is empty set the UUID as the default path
|
||||
if (
|
||||
[WEBHOOK_NODE_TYPE, FORM_TRIGGER_NODE_TYPE].includes(newNodeData.type) &&
|
||||
newNodeData.parameters.path === ''
|
||||
) {
|
||||
newNodeData.parameters.path = newNodeData.webhookId as string;
|
||||
}
|
||||
|
||||
workflowsStore.setNodePristine(newNodeData.name, true);
|
||||
uiStore.stateIsDirty = true;
|
||||
|
||||
|
|
|
@ -1253,6 +1253,7 @@ export function useNodeHelpers() {
|
|||
updateNodesCredentialsIssues,
|
||||
getNodeInputData,
|
||||
setSuccessOutput,
|
||||
matchCredentials,
|
||||
isInsertingNodes,
|
||||
credentialsUpdated,
|
||||
isProductionExecutionPreview,
|
||||
|
|
|
@ -279,9 +279,11 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
|||
if (node.name === options.destinationNode || !node.disabled) {
|
||||
let testUrl = '';
|
||||
|
||||
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
if (nodeType?.webhooks?.length) {
|
||||
testUrl = workflowHelpers.getWebhookUrl(nodeType.webhooks[0], node, 'test');
|
||||
if (node.type === FORM_TRIGGER_NODE_TYPE) {
|
||||
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
|
||||
if (nodeType?.webhooks?.length) {
|
||||
testUrl = workflowHelpers.getWebhookUrl(nodeType.webhooks[0], node, 'test');
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -433,10 +435,20 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
|
|||
}
|
||||
}
|
||||
|
||||
async function stopWaitingForWebhook() {
|
||||
try {
|
||||
await workflowsStore.removeTestWebhook(workflowsStore.workflowId);
|
||||
} catch (error) {
|
||||
toast.showError(error, i18n.baseText('nodeView.showError.stopWaitingForWebhook.title'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
consolidateRunDataAndStartNodes,
|
||||
runWorkflow,
|
||||
runWorkflowApi,
|
||||
stopCurrentExecution,
|
||||
stopWaitingForWebhook,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1050,12 +1050,8 @@ export function useWorkflowHelpers(options: { router: ReturnType<typeof useRoute
|
|||
}
|
||||
}
|
||||
|
||||
async function initState(workflowData: IWorkflowDb, set = false): Promise<void> {
|
||||
async function initState(workflowData: IWorkflowDb): Promise<void> {
|
||||
workflowsStore.addWorkflow(workflowData);
|
||||
if (set) {
|
||||
workflowsStore.setWorkflow(workflowData);
|
||||
}
|
||||
|
||||
workflowsStore.setActive(workflowData.active || false);
|
||||
workflowsStore.setWorkflowId(workflowData.id);
|
||||
workflowsStore.setWorkflowName({
|
||||
|
|
|
@ -58,12 +58,16 @@ export interface CanvasElementData {
|
|||
execution: {
|
||||
status?: ExecutionStatus;
|
||||
waiting?: string;
|
||||
running: boolean;
|
||||
};
|
||||
runData: {
|
||||
count: number;
|
||||
visible: boolean;
|
||||
};
|
||||
renderType: 'default' | 'trigger' | 'configuration' | 'configurable';
|
||||
render: {
|
||||
type: 'default';
|
||||
options: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
export type CanvasElement = Node<CanvasElementData>;
|
||||
|
@ -72,7 +76,7 @@ export interface CanvasConnectionData {
|
|||
source: CanvasConnectionPort;
|
||||
target: CanvasConnectionPort;
|
||||
fromNodeName?: string;
|
||||
status?: 'success' | 'error' | 'pinned';
|
||||
status?: 'success' | 'error' | 'pinned' | 'running';
|
||||
}
|
||||
|
||||
export type CanvasConnection = DefaultEdge<CanvasConnectionData>;
|
||||
|
@ -96,3 +100,5 @@ export interface CanvasNodeInjectionData {
|
|||
export interface CanvasNodeHandleInjectionData {
|
||||
label: Ref<string | undefined>;
|
||||
}
|
||||
|
||||
export type ConnectStartEvent = { handleId: string; handleType: string; nodeId: string };
|
||||
|
|
|
@ -27,13 +27,14 @@ import type {
|
|||
XYPosition,
|
||||
} from '@/Interface';
|
||||
import type { Connection } from '@vue-flow/core';
|
||||
import type { CanvasElement } from '@/types';
|
||||
import type { CanvasElement, ConnectStartEvent } from '@/types';
|
||||
import {
|
||||
CANVAS_AUTO_ADD_MANUAL_TRIGGER_EXPERIMENT,
|
||||
EnterpriseEditionFeature,
|
||||
MAIN_HEADER_TABS,
|
||||
MODAL_CANCEL,
|
||||
MODAL_CONFIRM,
|
||||
NODE_CREATOR_OPEN_SOURCES,
|
||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||
VIEWS,
|
||||
} from '@/constants';
|
||||
|
@ -73,7 +74,11 @@ import { useUsersStore } from '@/stores/users.store';
|
|||
import { sourceControlEventBus } from '@/event-bus/source-control';
|
||||
import { useTagsStore } from '@/stores/tags.store';
|
||||
import { usePushConnectionStore } from '@/stores/pushConnection.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { getNodeViewTab } from '@/utils/canvasUtils';
|
||||
import { parseCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
|
||||
import CanvasStopCurrentExecutionButton from '@/components/canvas/elements/buttons/CanvasStopCurrentExecutionButton.vue';
|
||||
import CanvasStopWaitingForWebhookButton from '@/components/canvas/elements/buttons/CanvasStopWaitingForWebhookButton.vue';
|
||||
|
||||
const NodeCreation = defineAsyncComponent(
|
||||
async () => await import('@/components/Node/NodeCreation.vue'),
|
||||
|
@ -115,10 +120,11 @@ const projectsStore = useProjectsStore();
|
|||
const usersStore = useUsersStore();
|
||||
const tagsStore = useTagsStore();
|
||||
const pushConnectionStore = usePushConnectionStore();
|
||||
const ndvStore = useNDVStore();
|
||||
|
||||
const lastClickPosition = ref<XYPosition>([450, 450]);
|
||||
|
||||
const { runWorkflow } = useRunWorkflow({ router });
|
||||
const { runWorkflow, stopCurrentExecution, stopWaitingForWebhook } = useRunWorkflow({ router });
|
||||
const {
|
||||
updateNodePosition,
|
||||
renameNode,
|
||||
|
@ -145,7 +151,6 @@ const readOnlyNotification = ref<null | { visible: boolean }>(null);
|
|||
|
||||
const isProductionExecutionPreview = ref(false);
|
||||
const isExecutionPreview = ref(false);
|
||||
const isExecutionWaitingForWebhook = ref(false);
|
||||
|
||||
const canOpenNDV = ref(true);
|
||||
const hideNodeIssues = ref(false);
|
||||
|
@ -318,7 +323,7 @@ async function runAutoAddManualTriggerExperiment() {
|
|||
}
|
||||
|
||||
function resetWorkspace() {
|
||||
onToggleNodeCreator({ createNodeActive: false });
|
||||
onOpenNodeCreator({ createNodeActive: false });
|
||||
nodeCreatorStore.setShowScrim(false);
|
||||
|
||||
// Make sure that if there is a waiting test-webhook that it gets removed
|
||||
|
@ -346,7 +351,9 @@ async function openWorkflow(data: IWorkflowDb) {
|
|||
|
||||
resetWorkspace();
|
||||
|
||||
await workflowHelpers.initState(data, true);
|
||||
await workflowHelpers.initState(data);
|
||||
await addNodes(data.nodes);
|
||||
workflowsStore.setConnections(data.connections);
|
||||
|
||||
if (data.sharedWithProjects) {
|
||||
workflowsEEStore.setWorkflowSharedWith({
|
||||
|
@ -483,6 +490,19 @@ function onCreateConnection(connection: Connection) {
|
|||
createConnection(connection);
|
||||
}
|
||||
|
||||
function onCreateConnectionCancelled(event: ConnectStartEvent) {
|
||||
const { type, index } = parseCanvasConnectionHandleString(event.handleId);
|
||||
setTimeout(() => {
|
||||
nodeCreatorStore.openNodeCreatorForConnectingNode({
|
||||
index,
|
||||
endpointUuid: event.handleId,
|
||||
eventSource: NODE_CREATOR_OPEN_SOURCES.NODE_CONNECTION_DROP,
|
||||
outputType: type,
|
||||
sourceId: event.nodeId,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onDeleteConnection(connection: Connection) {
|
||||
deleteConnection(connection, { trackHistory: true });
|
||||
}
|
||||
|
@ -522,11 +542,11 @@ async function onSwitchActiveNode(nodeName: string) {
|
|||
setNodeActiveByName(nodeName);
|
||||
}
|
||||
|
||||
async function onOpenConnectionNodeCreator(node: string, connectionType: NodeConnectionType) {
|
||||
async function onOpenSelectiveNodeCreator(node: string, connectionType: NodeConnectionType) {
|
||||
nodeCreatorStore.openSelectiveNodeCreator({ node, connectionType });
|
||||
}
|
||||
|
||||
function onToggleNodeCreator(options: ToggleNodeCreatorOptions) {
|
||||
function onOpenNodeCreator(options: ToggleNodeCreatorOptions) {
|
||||
nodeCreatorStore.openNodeCreator(options);
|
||||
}
|
||||
|
||||
|
@ -534,6 +554,18 @@ function onToggleNodeCreator(options: ToggleNodeCreatorOptions) {
|
|||
* Executions
|
||||
*/
|
||||
|
||||
const isStoppingExecution = ref(false);
|
||||
|
||||
const isWorkflowRunning = computed(() => uiStore.isActionActive.workflowRunning);
|
||||
const isExecutionWaitingForWebhook = computed(() => workflowsStore.executionWaitingForWebhook);
|
||||
|
||||
const isStopExecutionButtonVisible = computed(
|
||||
() => isWorkflowRunning.value && !isExecutionWaitingForWebhook.value,
|
||||
);
|
||||
const isStopWaitingForWebhookButtonVisible = computed(
|
||||
() => isWorkflowRunning.value && isExecutionWaitingForWebhook.value,
|
||||
);
|
||||
|
||||
async function onRunWorkflow() {
|
||||
trackRunWorkflow();
|
||||
|
||||
|
@ -557,12 +589,42 @@ function trackRunWorkflow() {
|
|||
});
|
||||
}
|
||||
|
||||
async function onRunWorkflowToNode(id: string) {
|
||||
const node = workflowsStore.getNodeById(id);
|
||||
if (!node) return;
|
||||
|
||||
trackRunWorkflowToNode(node);
|
||||
await runWorkflow({ destinationNode: node.name, source: 'Node.executeNode' });
|
||||
}
|
||||
|
||||
function trackRunWorkflowToNode(node: INodeUi) {
|
||||
const telemetryPayload = {
|
||||
node_type: node.type,
|
||||
workflow_id: workflowsStore.workflowId,
|
||||
source: 'canvas',
|
||||
push_ref: ndvStore.pushRef,
|
||||
};
|
||||
|
||||
telemetry.track('User clicked execute node button', telemetryPayload);
|
||||
void externalHooks.run('nodeView.onRunNode', telemetryPayload);
|
||||
}
|
||||
|
||||
async function openExecution(_executionId: string) {
|
||||
// @TODO
|
||||
}
|
||||
|
||||
async function onStopExecution() {
|
||||
isStoppingExecution.value = true;
|
||||
await stopCurrentExecution();
|
||||
isStoppingExecution.value = false;
|
||||
}
|
||||
|
||||
async function onStopWaitingForWebhook() {
|
||||
await stopWaitingForWebhook();
|
||||
}
|
||||
|
||||
/**
|
||||
* Keboard
|
||||
* Keyboard
|
||||
*/
|
||||
|
||||
function addKeyboardEventBindings() {
|
||||
|
@ -909,20 +971,35 @@ onBeforeUnmount(() => {
|
|||
@update:node:active="onSetNodeActive"
|
||||
@update:node:selected="onSetNodeSelected"
|
||||
@update:node:enabled="onToggleNodeDisabled"
|
||||
@run:node="onRunWorkflowToNode"
|
||||
@delete:node="onDeleteNode"
|
||||
@create:connection="onCreateConnection"
|
||||
@create:connection:cancelled="onCreateConnectionCancelled"
|
||||
@delete:connection="onDeleteConnection"
|
||||
@click:pane="onClickPane"
|
||||
>
|
||||
<div :class="$style.executionButtons">
|
||||
<CanvasExecuteWorkflowButton @click="onRunWorkflow" />
|
||||
<CanvasExecuteWorkflowButton
|
||||
:waiting-for-webhook="isExecutionWaitingForWebhook"
|
||||
:executing="isWorkflowRunning"
|
||||
@click="onRunWorkflow"
|
||||
/>
|
||||
<CanvasStopCurrentExecutionButton
|
||||
v-if="isStopExecutionButtonVisible"
|
||||
:stopping="isStoppingExecution"
|
||||
@click="onStopExecution"
|
||||
/>
|
||||
<CanvasStopWaitingForWebhookButton
|
||||
v-if="isStopWaitingForWebhookButtonVisible"
|
||||
@click="onStopWaitingForWebhook"
|
||||
/>
|
||||
</div>
|
||||
<Suspense>
|
||||
<NodeCreation
|
||||
v-if="!isReadOnlyRoute && !isReadOnlyEnvironment"
|
||||
:create-node-active="uiStore.isCreateNodeActive"
|
||||
:node-view-scale="1"
|
||||
@toggle-node-creator="onToggleNodeCreator"
|
||||
@toggle-node-creator="onOpenNodeCreator"
|
||||
@add-nodes="onAddNodesAndConnections"
|
||||
/>
|
||||
</Suspense>
|
||||
|
@ -933,12 +1010,12 @@ onBeforeUnmount(() => {
|
|||
:is-production-execution-preview="isProductionExecutionPreview"
|
||||
:renaming="false"
|
||||
@value-changed="onRenameNode"
|
||||
@stop-execution="onStopExecution"
|
||||
@switch-selected-node="onSwitchActiveNode"
|
||||
@open-connection-node-creator="onOpenConnectionNodeCreator"
|
||||
@open-connection-node-creator="onOpenSelectiveNodeCreator"
|
||||
/>
|
||||
<!--
|
||||
:renaming="renamingActive"
|
||||
@stop-execution="stopExecution"
|
||||
@save-keyboard-shortcut="onSaveKeyboardShortcut"
|
||||
-->
|
||||
</Suspense>
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
# License
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- Content of branches other than the main branch (i.e. "master") are not licensed.
|
||||
- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License.
|
||||
To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License
|
||||
specifically allowing you access to such source code files and as defined in "LICENSE_EE.md".
|
||||
- All third party components incorporated into the n8n Software are licensed under the original license
|
||||
provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
|
||||
License" as defined below.
|
||||
|
||||
## Sustainable Use License
|
||||
|
||||
Version 1.0
|
||||
|
||||
### Acceptance
|
||||
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
### Copyright License
|
||||
|
||||
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
||||
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
||||
to the limitations below.
|
||||
|
||||
### Limitations
|
||||
|
||||
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
||||
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
||||
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
||||
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
||||
|
||||
### Patents
|
||||
|
||||
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
||||
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
||||
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
||||
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
||||
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
||||
license ends immediately for work on behalf of your company.
|
||||
|
||||
### Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
||||
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
||||
stating that you have modified the software.
|
||||
|
||||
### No Other Rights
|
||||
|
||||
These terms do not imply any licenses other than those expressly granted in these terms.
|
||||
|
||||
### Termination
|
||||
|
||||
If you use the software in violation of these terms, such use is not licensed, and your license will
|
||||
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
||||
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
||||
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
||||
terms will cause your license to terminate automatically and permanently.
|
||||
|
||||
### No Liability
|
||||
|
||||
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
||||
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
||||
any kind of legal claim.
|
||||
|
||||
### Definitions
|
||||
|
||||
The “licensor” is the entity offering these terms.
|
||||
|
||||
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
||||
|
||||
“You” refers to the individual or entity agreeing to these terms.
|
||||
|
||||
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
||||
all organizations that have control over, are under the control of, or are under common control with that
|
||||
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
||||
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
“Your license” is the license granted to you for the software under these terms.
|
||||
|
||||
“Use” means anything you do with the software requiring your license.
|
||||
|
||||
“Trademark” means trademarks, service marks, and similar rights.
|
|
@ -1,27 +0,0 @@
|
|||
# The n8n Enterprise License (the “Enterprise License”)
|
||||
|
||||
Copyright (c) 2022-present n8n GmbH.
|
||||
|
||||
With regard to the n8n Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be used in production, if
|
||||
you (and any entity that you represent) hold a valid n8n Enterprise license corresponding to your
|
||||
usage. Subject to the foregoing sentence, you are free to modify this Software and publish patches
|
||||
to the Software. You agree that n8n and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications and/or patches
|
||||
may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid n8n
|
||||
Enterprise license for the corresponding usage. Notwithstanding the foregoing, you may copy and
|
||||
modify the Software for development and testing purposes, without requiring a subscription. You
|
||||
agree that n8n and/or its licensors (as applicable) retain all right, title and interest in and to
|
||||
all such modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or
|
||||
sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the n8n Software, those components are licensed
|
||||
under the original license provided by the owner of the applicable component.
|
|
@ -2,16 +2,6 @@
|
|||
"name": "n8n-node-dev",
|
||||
"version": "1.49.0",
|
||||
"description": "CLI to simplify n8n credentials/node development",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"main": "dist/src/index",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"oclif": {
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
# License
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- Content of branches other than the main branch (i.e. "master") are not licensed.
|
||||
- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License.
|
||||
To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License
|
||||
specifically allowing you access to such source code files and as defined in "LICENSE_EE.md".
|
||||
- All third party components incorporated into the n8n Software are licensed under the original license
|
||||
provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
|
||||
License" as defined below.
|
||||
|
||||
## Sustainable Use License
|
||||
|
||||
Version 1.0
|
||||
|
||||
### Acceptance
|
||||
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
### Copyright License
|
||||
|
||||
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
||||
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
||||
to the limitations below.
|
||||
|
||||
### Limitations
|
||||
|
||||
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
||||
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
||||
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
||||
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
||||
|
||||
### Patents
|
||||
|
||||
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
||||
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
||||
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
||||
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
||||
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
||||
license ends immediately for work on behalf of your company.
|
||||
|
||||
### Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
||||
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
||||
stating that you have modified the software.
|
||||
|
||||
### No Other Rights
|
||||
|
||||
These terms do not imply any licenses other than those expressly granted in these terms.
|
||||
|
||||
### Termination
|
||||
|
||||
If you use the software in violation of these terms, such use is not licensed, and your license will
|
||||
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
||||
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
||||
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
||||
terms will cause your license to terminate automatically and permanently.
|
||||
|
||||
### No Liability
|
||||
|
||||
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
||||
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
||||
any kind of legal claim.
|
||||
|
||||
### Definitions
|
||||
|
||||
The “licensor” is the entity offering these terms.
|
||||
|
||||
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
||||
|
||||
“You” refers to the individual or entity agreeing to these terms.
|
||||
|
||||
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
||||
all organizations that have control over, are under the control of, or are under common control with that
|
||||
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
||||
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
“Your license” is the license granted to you for the software under these terms.
|
||||
|
||||
“Use” means anything you do with the software requiring your license.
|
||||
|
||||
“Trademark” means trademarks, service marks, and similar rights.
|
|
@ -1,27 +0,0 @@
|
|||
# The n8n Enterprise License (the “Enterprise License”)
|
||||
|
||||
Copyright (c) 2022-present n8n GmbH.
|
||||
|
||||
With regard to the n8n Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be used in production, if
|
||||
you (and any entity that you represent) hold a valid n8n Enterprise license corresponding to your
|
||||
usage. Subject to the foregoing sentence, you are free to modify this Software and publish patches
|
||||
to the Software. You agree that n8n and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications and/or patches
|
||||
may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid n8n
|
||||
Enterprise license for the corresponding usage. Notwithstanding the foregoing, you may copy and
|
||||
modify the Software for development and testing purposes, without requiring a subscription. You
|
||||
agree that n8n and/or its licensors (as applicable) retain all right, title and interest in and to
|
||||
all such modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or
|
||||
sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the n8n Software, those components are licensed
|
||||
under the original license provided by the owner of the applicable component.
|
|
@ -8,6 +8,13 @@ export class OrbitApi implements ICredentialType {
|
|||
documentationUrl = 'orbit';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName:
|
||||
'Orbit has been shutdown and will no longer function from July 11th, You can read more <a target="_blank" href="https://orbit.love/blog/orbit-is-joining-postman">here</a>.',
|
||||
name: 'deprecated',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'API Token',
|
||||
name: 'accessToken',
|
||||
|
|
|
@ -42,7 +42,7 @@ export const activityFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -97,7 +97,7 @@ export const activityFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getActivityTypes',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
description:
|
||||
'A user-defined way to group activities of the same nature. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||
},
|
||||
|
@ -151,7 +151,7 @@ export const activityFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
|
|
@ -66,7 +66,7 @@ export const memberFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -101,7 +101,7 @@ export const memberFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -151,7 +151,7 @@ export const memberFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -257,7 +257,7 @@ export const memberFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -398,7 +398,7 @@ export const memberFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -528,7 +528,7 @@ export const memberFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
|
|
@ -48,7 +48,7 @@ export const noteFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -96,7 +96,7 @@ export const noteFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -174,7 +174,7 @@ export const noteFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
|
@ -8,8 +7,7 @@ import type {
|
|||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import { orbitApiRequest, orbitApiRequestAllItems, resolveIdentities } from './GenericFunctions';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { activityFields, activityOperations } from './ActivityDescription';
|
||||
|
||||
|
@ -19,8 +17,6 @@ import { noteFields, noteOperations } from './NoteDescription';
|
|||
|
||||
import { postFields, postOperations } from './PostDescription';
|
||||
|
||||
import type { IRelation } from './Interfaces';
|
||||
|
||||
export class Orbit implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Orbit',
|
||||
|
@ -30,6 +26,7 @@ export class Orbit implements INodeType {
|
|||
version: 1,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Consume Orbit API',
|
||||
hidden: true,
|
||||
defaults: {
|
||||
name: 'Orbit',
|
||||
},
|
||||
|
@ -42,6 +39,13 @@ export class Orbit implements INodeType {
|
|||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName:
|
||||
'Orbit has been shutdown and will no longer function from July 11th, You can read more <a target="_blank" href="https://orbit.love/blog/orbit-is-joining-postman">here</a>.',
|
||||
name: 'deprecated',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
|
@ -85,463 +89,18 @@ export class Orbit implements INodeType {
|
|||
methods = {
|
||||
loadOptions: {
|
||||
async getWorkspaces(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const workspaces = await orbitApiRequest.call(this, 'GET', '/workspaces');
|
||||
for (const workspace of workspaces.data) {
|
||||
returnData.push({
|
||||
name: workspace.attributes.name,
|
||||
value: workspace.attributes.slug,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
return [{ name: 'Deprecated', value: 'Deprecated' }];
|
||||
},
|
||||
async getActivityTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const { data } = await orbitApiRequest.call(this, 'GET', '/activity_types');
|
||||
for (const activityType of data) {
|
||||
returnData.push({
|
||||
name: activityType.attributes.short_name,
|
||||
value: activityType.id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
return [{ name: 'Deprecated', value: 'Deprecated' }];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
const length = items.length;
|
||||
const qs: IDataObject = {};
|
||||
let responseData;
|
||||
const resource = this.getNodeParameter('resource', 0);
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
for (let i = 0; i < length; i++) {
|
||||
try {
|
||||
if (resource === 'activity') {
|
||||
if (operation === 'create') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const memberId = this.getNodeParameter('memberId', i) as string;
|
||||
const title = this.getNodeParameter('title', i) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
const body: IDataObject = {
|
||||
title,
|
||||
};
|
||||
if (additionalFields.description) {
|
||||
body.description = additionalFields.description as string;
|
||||
}
|
||||
if (additionalFields.link) {
|
||||
body.link = additionalFields.link as string;
|
||||
}
|
||||
if (additionalFields.linkText) {
|
||||
body.link_text = additionalFields.linkText as string;
|
||||
}
|
||||
if (additionalFields.activityType) {
|
||||
body.activity_type = additionalFields.activityType as string;
|
||||
}
|
||||
if (additionalFields.key) {
|
||||
body.key = additionalFields.key as string;
|
||||
}
|
||||
if (additionalFields.occurredAt) {
|
||||
body.occurred_at = additionalFields.occurredAt as string;
|
||||
}
|
||||
|
||||
responseData = await orbitApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/${workspaceId}/members/${memberId}/activities`,
|
||||
body,
|
||||
);
|
||||
responseData = responseData.data;
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
const filters = this.getNodeParameter('filters', i);
|
||||
let endpoint = `/${workspaceId}/activities`;
|
||||
if (filters.memberId) {
|
||||
endpoint = `/${workspaceId}/members/${filters.memberId}/activities`;
|
||||
}
|
||||
if (returnAll) {
|
||||
responseData = await orbitApiRequestAllItems.call(
|
||||
this,
|
||||
'data',
|
||||
'GET',
|
||||
endpoint,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
} else {
|
||||
qs.limit = this.getNodeParameter('limit', 0);
|
||||
responseData = await orbitApiRequestAllItems.call(
|
||||
this,
|
||||
'data',
|
||||
'GET',
|
||||
endpoint,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
responseData = responseData.splice(0, qs.limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resource === 'member') {
|
||||
if (operation === 'upsert') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
const member: IDataObject = {};
|
||||
const identity: IDataObject = {};
|
||||
if (additionalFields.bio) {
|
||||
member.bio = additionalFields.bio as string;
|
||||
}
|
||||
if (additionalFields.birthday) {
|
||||
member.birthday = moment(additionalFields.birthday as string).format('MM-DD-YYYY');
|
||||
}
|
||||
if (additionalFields.company) {
|
||||
member.company = additionalFields.company as string;
|
||||
}
|
||||
if (additionalFields.location) {
|
||||
member.location = additionalFields.location as string;
|
||||
}
|
||||
if (additionalFields.name) {
|
||||
member.name = additionalFields.name as string;
|
||||
}
|
||||
if (additionalFields.bio) {
|
||||
member.bio = additionalFields.bio as string;
|
||||
}
|
||||
if (additionalFields.pronouns) {
|
||||
member.pronouns = additionalFields.pronouns as string;
|
||||
}
|
||||
if (additionalFields.shippingAddress) {
|
||||
member.shipping_address = additionalFields.shippingAddress as string;
|
||||
}
|
||||
if (additionalFields.slug) {
|
||||
member.slug = additionalFields.slug as string;
|
||||
}
|
||||
if (additionalFields.tagsToAdd) {
|
||||
member.tags_to_add = additionalFields.tagsToAdd as string;
|
||||
}
|
||||
if (additionalFields.tagList) {
|
||||
member.tag_list = additionalFields.tagList as string;
|
||||
}
|
||||
if (additionalFields.tshirt) {
|
||||
member.tshirt = additionalFields.tshirt as string;
|
||||
}
|
||||
if (additionalFields.hasOwnProperty('teammate')) {
|
||||
member.teammate = additionalFields.teammate as boolean;
|
||||
}
|
||||
if (additionalFields.url) {
|
||||
member.url = additionalFields.url as string;
|
||||
}
|
||||
|
||||
const data = (this.getNodeParameter('identityUi', i) as IDataObject)
|
||||
.identityValue as IDataObject;
|
||||
if (data) {
|
||||
if (['github', 'twitter', 'discourse'].includes(data.source as string)) {
|
||||
identity.source = data.source as string;
|
||||
const searchBy = data.searchBy as string;
|
||||
if (searchBy === 'id') {
|
||||
identity.uid = data.id as string;
|
||||
} else {
|
||||
identity.username = data.username as string;
|
||||
}
|
||||
if (data.source === 'discourse') {
|
||||
identity.source_host = data.host as string;
|
||||
}
|
||||
} else {
|
||||
//it's email
|
||||
identity.email = data.email as string;
|
||||
}
|
||||
}
|
||||
|
||||
responseData = await orbitApiRequest.call(this, 'POST', `/${workspaceId}/members`, {
|
||||
member,
|
||||
identity,
|
||||
});
|
||||
responseData = responseData.data;
|
||||
}
|
||||
if (operation === 'delete') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const memberId = this.getNodeParameter('memberId', i) as string;
|
||||
responseData = await orbitApiRequest.call(
|
||||
this,
|
||||
'DELETE',
|
||||
`/${workspaceId}/members/${memberId}`,
|
||||
);
|
||||
responseData = { success: true };
|
||||
}
|
||||
if (operation === 'get') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const memberId = this.getNodeParameter('memberId', i) as string;
|
||||
const resolve = this.getNodeParameter('resolveIdentities', 0) as boolean;
|
||||
responseData = await orbitApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/${workspaceId}/members/${memberId}`,
|
||||
);
|
||||
if (resolve) {
|
||||
resolveIdentities(responseData as IRelation);
|
||||
}
|
||||
responseData = responseData.data;
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const returnAll = this.getNodeParameter('returnAll', 0);
|
||||
const options = this.getNodeParameter('options', i);
|
||||
Object.assign(qs, options);
|
||||
qs.resolveIdentities = this.getNodeParameter('resolveIdentities', 0) as boolean;
|
||||
if (returnAll) {
|
||||
responseData = await orbitApiRequestAllItems.call(
|
||||
this,
|
||||
'data',
|
||||
'GET',
|
||||
`/${workspaceId}/members`,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
} else {
|
||||
qs.limit = this.getNodeParameter('limit', 0);
|
||||
responseData = await orbitApiRequestAllItems.call(
|
||||
this,
|
||||
'data',
|
||||
'GET',
|
||||
`/${workspaceId}/members`,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
responseData = responseData.splice(0, qs.limit);
|
||||
}
|
||||
}
|
||||
if (operation === 'lookup') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const source = this.getNodeParameter('source', i) as string;
|
||||
|
||||
if (['github', 'twitter', 'discourse'].includes(source)) {
|
||||
qs.source = this.getNodeParameter('source', i) as string;
|
||||
const searchBy = this.getNodeParameter('searchBy', i) as string;
|
||||
if (searchBy === 'id') {
|
||||
qs.uid = this.getNodeParameter('id', i) as string;
|
||||
} else {
|
||||
qs.username = this.getNodeParameter('username', i) as string;
|
||||
}
|
||||
if (source === 'discourse') {
|
||||
qs.source_host = this.getNodeParameter('host', i) as string;
|
||||
}
|
||||
} else {
|
||||
//it's email
|
||||
qs.email = this.getNodeParameter('email', i) as string;
|
||||
}
|
||||
|
||||
responseData = await orbitApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/${workspaceId}/members/find`,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
responseData = responseData.data;
|
||||
}
|
||||
if (operation === 'update') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const memberId = this.getNodeParameter('memberId', i) as string;
|
||||
const updateFields = this.getNodeParameter('updateFields', i);
|
||||
const body: IDataObject = {};
|
||||
if (updateFields.bio) {
|
||||
body.bio = updateFields.bio as string;
|
||||
}
|
||||
if (updateFields.birthday) {
|
||||
body.birthday = moment(updateFields.birthday as string).format('MM-DD-YYYY');
|
||||
}
|
||||
if (updateFields.company) {
|
||||
body.company = updateFields.company as string;
|
||||
}
|
||||
if (updateFields.location) {
|
||||
body.location = updateFields.location as string;
|
||||
}
|
||||
if (updateFields.name) {
|
||||
body.name = updateFields.name as string;
|
||||
}
|
||||
if (updateFields.bio) {
|
||||
body.bio = updateFields.bio as string;
|
||||
}
|
||||
if (updateFields.pronouns) {
|
||||
body.pronouns = updateFields.pronouns as string;
|
||||
}
|
||||
if (updateFields.shippingAddress) {
|
||||
body.shipping_address = updateFields.shippingAddress as string;
|
||||
}
|
||||
if (updateFields.slug) {
|
||||
body.slug = updateFields.slug as string;
|
||||
}
|
||||
if (updateFields.tagsToAdd) {
|
||||
body.tags_to_add = updateFields.tagsToAdd as string;
|
||||
}
|
||||
if (updateFields.tagList) {
|
||||
body.tag_list = updateFields.tagList as string;
|
||||
}
|
||||
if (updateFields.tshirt) {
|
||||
body.tshirt = updateFields.tshirt as string;
|
||||
}
|
||||
if (updateFields.hasOwnProperty('teammate')) {
|
||||
body.teammate = updateFields.teammate as boolean;
|
||||
}
|
||||
if (updateFields.url) {
|
||||
body.url = updateFields.url as string;
|
||||
}
|
||||
|
||||
responseData = await orbitApiRequest.call(
|
||||
this,
|
||||
'PUT',
|
||||
`/${workspaceId}/members/${memberId}`,
|
||||
body,
|
||||
);
|
||||
responseData = { success: true };
|
||||
}
|
||||
}
|
||||
if (resource === 'note') {
|
||||
if (operation === 'create') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const memberId = this.getNodeParameter('memberId', i) as string;
|
||||
const note = this.getNodeParameter('note', i) as string;
|
||||
|
||||
responseData = await orbitApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/${workspaceId}/members/${memberId}/notes`,
|
||||
{ body: note },
|
||||
);
|
||||
responseData = responseData.data;
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const memberId = this.getNodeParameter('memberId', i) as string;
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
qs.resolveMember = this.getNodeParameter('resolveMember', 0) as boolean;
|
||||
if (returnAll) {
|
||||
responseData = await orbitApiRequestAllItems.call(
|
||||
this,
|
||||
'data',
|
||||
'GET',
|
||||
`/${workspaceId}/members/${memberId}/notes`,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
} else {
|
||||
qs.limit = this.getNodeParameter('limit', 0);
|
||||
responseData = await orbitApiRequestAllItems.call(
|
||||
this,
|
||||
'data',
|
||||
'GET',
|
||||
`/${workspaceId}/members/${memberId}/notes`,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
responseData = responseData.splice(0, qs.limit);
|
||||
}
|
||||
}
|
||||
if (operation === 'update') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const memberId = this.getNodeParameter('memberId', i) as string;
|
||||
const noteId = this.getNodeParameter('noteId', i) as string;
|
||||
const note = this.getNodeParameter('note', i) as string;
|
||||
|
||||
responseData = await orbitApiRequest.call(
|
||||
this,
|
||||
'PUT',
|
||||
`/${workspaceId}/members/${memberId}/notes/${noteId}`,
|
||||
{ body: note },
|
||||
);
|
||||
responseData = { success: true };
|
||||
}
|
||||
}
|
||||
if (resource === 'post') {
|
||||
if (operation === 'create') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const memberId = this.getNodeParameter('memberId', i) as string;
|
||||
const url = this.getNodeParameter('url', i) as string;
|
||||
const additionalFields = this.getNodeParameter('additionalFields', i);
|
||||
const body: IDataObject = {
|
||||
type: 'post',
|
||||
activity_type: 'post',
|
||||
url,
|
||||
};
|
||||
if (additionalFields.publishedAt) {
|
||||
body.occurred_at = additionalFields.publishedAt as string;
|
||||
delete body.publishedAt;
|
||||
}
|
||||
|
||||
responseData = await orbitApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
`/${workspaceId}/members/${memberId}/activities/`,
|
||||
body,
|
||||
);
|
||||
responseData = responseData.data;
|
||||
}
|
||||
if (operation === 'getAll') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const returnAll = this.getNodeParameter('returnAll', i);
|
||||
const filters = this.getNodeParameter('filters', i);
|
||||
let endpoint = `/${workspaceId}/activities`;
|
||||
qs.type = 'content';
|
||||
if (filters.memberId) {
|
||||
endpoint = `/${workspaceId}/members/${filters.memberId}/activities`;
|
||||
}
|
||||
if (returnAll) {
|
||||
responseData = await orbitApiRequestAllItems.call(
|
||||
this,
|
||||
'data',
|
||||
'GET',
|
||||
endpoint,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
} else {
|
||||
qs.limit = this.getNodeParameter('limit', 0);
|
||||
responseData = await orbitApiRequestAllItems.call(
|
||||
this,
|
||||
'data',
|
||||
'GET',
|
||||
endpoint,
|
||||
{},
|
||||
qs,
|
||||
);
|
||||
responseData = responseData.splice(0, qs.limit);
|
||||
}
|
||||
}
|
||||
if (operation === 'delete') {
|
||||
const workspaceId = this.getNodeParameter('workspaceId', i) as string;
|
||||
const memberId = this.getNodeParameter('memberId', i) as string;
|
||||
const postId = this.getNodeParameter('postId', i) as string;
|
||||
|
||||
responseData = await orbitApiRequest.call(
|
||||
this,
|
||||
'DELETE',
|
||||
`/${workspaceId}/members/${memberId}/activities/${postId}`,
|
||||
);
|
||||
responseData = { success: true };
|
||||
}
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(responseData as IDataObject),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
returnData.push(...executionData);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail(error)) {
|
||||
const executionErrorData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ error: error.message }),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
returnData.push(...executionErrorData);
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return [returnData];
|
||||
throw new NodeApiError(this.getNode(), {
|
||||
message: 'Service is deprecated, From July 11th Orbit will no longer function.',
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ export const postFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -119,7 +119,7 @@ export const postFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
@ -194,7 +194,7 @@ export const postFields: INodeProperties[] = [
|
|||
typeOptions: {
|
||||
loadOptionsMethod: 'getWorkspaces',
|
||||
},
|
||||
default: '',
|
||||
default: 'Deprecated',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
|
|
|
@ -1289,6 +1289,26 @@ export class Telegram implements INodeType {
|
|||
default: '',
|
||||
description: 'HTTP or tg:// URL to be opened when button is pressed',
|
||||
},
|
||||
{
|
||||
displayName: 'Web App',
|
||||
name: 'web_app',
|
||||
type: 'collection',
|
||||
placeholder: 'Set Telegram Web App URL',
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'An HTTPS URL of a Web App to be opened',
|
||||
},
|
||||
],
|
||||
description: 'Launch the Telegram Web App',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -1365,6 +1385,26 @@ export class Telegram implements INodeType {
|
|||
default: false,
|
||||
description: "Whether the user's request_location",
|
||||
},
|
||||
{
|
||||
displayName: 'Web App',
|
||||
name: 'web_app',
|
||||
type: 'collection',
|
||||
placeholder: 'Set Telegram Web App URL',
|
||||
typeOptions: {
|
||||
multipleValues: false,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'An HTTPS URL of a Web App to be opened',
|
||||
},
|
||||
],
|
||||
description: 'Launch the Telegram Web App',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -2,17 +2,7 @@
|
|||
"name": "n8n-nodes-base",
|
||||
"version": "1.49.0",
|
||||
"description": "Base nodes of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
},
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
# License
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
- Content of branches other than the main branch (i.e. "master") are not licensed.
|
||||
- Source code files that contain ".ee." in their filename are NOT licensed under the Sustainable Use License.
|
||||
To use source code files that contain ".ee." in their filename you must hold a valid n8n Enterprise License
|
||||
specifically allowing you access to such source code files and as defined in "LICENSE_EE.md".
|
||||
- All third party components incorporated into the n8n Software are licensed under the original license
|
||||
provided by the owner of the applicable component.
|
||||
- Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
|
||||
License" as defined below.
|
||||
|
||||
## Sustainable Use License
|
||||
|
||||
Version 1.0
|
||||
|
||||
### Acceptance
|
||||
|
||||
By using the software, you agree to all of the terms and conditions below.
|
||||
|
||||
### Copyright License
|
||||
|
||||
The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
|
||||
to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
|
||||
to the limitations below.
|
||||
|
||||
### Limitations
|
||||
|
||||
You may use or modify the software only for your own internal business purposes or for non-commercial or
|
||||
personal use. You may distribute the software or provide it to others only if you do so free of charge for
|
||||
non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
|
||||
the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
|
||||
|
||||
### Patents
|
||||
|
||||
The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
|
||||
license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
|
||||
subject to the limitations and conditions in this license. This license does not cover any patent claims that
|
||||
you cause to be infringed by modifications or additions to the software. If you or your company make any
|
||||
written claim that the software infringes or contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If your company makes such a claim, your patent
|
||||
license ends immediately for work on behalf of your company.
|
||||
|
||||
### Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
|
||||
terms. If you modify the software, you must include in any modified copies of the software a prominent notice
|
||||
stating that you have modified the software.
|
||||
|
||||
### No Other Rights
|
||||
|
||||
These terms do not imply any licenses other than those expressly granted in these terms.
|
||||
|
||||
### Termination
|
||||
|
||||
If you use the software in violation of these terms, such use is not licensed, and your license will
|
||||
automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
|
||||
violation of this license no later than 30 days after you receive that notice, your license will be reinstated
|
||||
retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
|
||||
terms will cause your license to terminate automatically and permanently.
|
||||
|
||||
### No Liability
|
||||
|
||||
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
|
||||
not be liable to you for any damages arising out of these terms or the use or nature of the software, under
|
||||
any kind of legal claim.
|
||||
|
||||
### Definitions
|
||||
|
||||
The “licensor” is the entity offering these terms.
|
||||
|
||||
The “software” is the software the licensor makes available under these terms, including any portion of it.
|
||||
|
||||
“You” refers to the individual or entity agreeing to these terms.
|
||||
|
||||
“Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
|
||||
all organizations that have control over, are under the control of, or are under common control with that
|
||||
organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
|
||||
management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
“Your license” is the license granted to you for the software under these terms.
|
||||
|
||||
“Use” means anything you do with the software requiring your license.
|
||||
|
||||
“Trademark” means trademarks, service marks, and similar rights.
|
|
@ -1,27 +0,0 @@
|
|||
# The n8n Enterprise License (the “Enterprise License”)
|
||||
|
||||
Copyright (c) 2022-present n8n GmbH.
|
||||
|
||||
With regard to the n8n Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be used in production, if
|
||||
you (and any entity that you represent) hold a valid n8n Enterprise license corresponding to your
|
||||
usage. Subject to the foregoing sentence, you are free to modify this Software and publish patches
|
||||
to the Software. You agree that n8n and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications and/or patches
|
||||
may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid n8n
|
||||
Enterprise license for the corresponding usage. Notwithstanding the foregoing, you may copy and
|
||||
modify the Software for development and testing purposes, without requiring a subscription. You
|
||||
agree that n8n and/or its licensors (as applicable) retain all right, title and interest in and to
|
||||
all such modifications. You are not granted any other rights beyond what is expressly stated herein.
|
||||
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or
|
||||
sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the n8n Software, those components are licensed
|
||||
under the original license provided by the owner of the applicable component.
|
|
@ -2,16 +2,6 @@
|
|||
"name": "n8n-workflow",
|
||||
"version": "1.48.0",
|
||||
"description": "Workflow base code of n8n",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://n8n.io",
|
||||
"author": {
|
||||
"name": "Jan Oberhauser",
|
||||
"email": "jan@n8n.io"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/n8n-io/n8n.git"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
|
|
|
@ -1424,13 +1424,6 @@ export class Workflow {
|
|||
return { data: null };
|
||||
}
|
||||
|
||||
if (triggerResponse.manualTriggerFunction !== undefined) {
|
||||
// If a manual trigger function is defined call it and wait till it did run
|
||||
await triggerResponse.manualTriggerFunction();
|
||||
}
|
||||
|
||||
const response = await triggerResponse.manualTriggerResponse!;
|
||||
|
||||
let closeFunction;
|
||||
if (triggerResponse.closeFunction) {
|
||||
// In manual mode we return the trigger closeFunction. That allows it to be called directly
|
||||
|
@ -1439,8 +1432,18 @@ export class Workflow {
|
|||
// If we would not be able to wait for it to close would it cause problems with "own" mode as the
|
||||
// process would be killed directly after it and so the acknowledge would not have been finished yet.
|
||||
closeFunction = triggerResponse.closeFunction;
|
||||
|
||||
// Manual testing of Trigger nodes creates an execution. If the execution is cancelled, `closeFunction` should be called to cleanup any open connections/consumers
|
||||
abortSignal?.addEventListener('abort', closeFunction);
|
||||
}
|
||||
|
||||
if (triggerResponse.manualTriggerFunction !== undefined) {
|
||||
// If a manual trigger function is defined call it and wait till it did run
|
||||
await triggerResponse.manualTriggerFunction();
|
||||
}
|
||||
|
||||
const response = await triggerResponse.manualTriggerResponse!;
|
||||
|
||||
if (response.length === 0) {
|
||||
return { data: null, closeFunction };
|
||||
}
|
||||
|
|
|
@ -4,13 +4,18 @@ import type {
|
|||
IBinaryKeyData,
|
||||
IConnections,
|
||||
IDataObject,
|
||||
IExecuteData,
|
||||
INode,
|
||||
INodeExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
INodeTypes,
|
||||
IRunExecutionData,
|
||||
ITriggerFunctions,
|
||||
ITriggerResponse,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeParameterValueType,
|
||||
} from '@/Interfaces';
|
||||
import { Workflow, type WorkflowParameters } from '@/Workflow';
|
||||
|
@ -2015,4 +2020,67 @@ describe('Workflow', () => {
|
|||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('runNode', () => {
|
||||
const nodeTypes = mock<INodeTypes>();
|
||||
const triggerNode = mock<INode>();
|
||||
const triggerResponse = mock<ITriggerResponse>({
|
||||
closeFunction: jest.fn(),
|
||||
// This node should never trigger, or return
|
||||
manualTriggerFunction: async () => await new Promise(() => {}),
|
||||
});
|
||||
const triggerNodeType = mock<INodeType>({
|
||||
description: {
|
||||
properties: [],
|
||||
},
|
||||
execute: undefined,
|
||||
poll: undefined,
|
||||
webhook: undefined,
|
||||
async trigger(this: ITriggerFunctions) {
|
||||
return triggerResponse;
|
||||
},
|
||||
});
|
||||
|
||||
nodeTypes.getByNameAndVersion.mockReturnValue(triggerNodeType);
|
||||
|
||||
const workflow = new Workflow({
|
||||
nodeTypes,
|
||||
nodes: [triggerNode],
|
||||
connections: {},
|
||||
active: false,
|
||||
});
|
||||
|
||||
const executionData = mock<IExecuteData>();
|
||||
const runExecutionData = mock<IRunExecutionData>();
|
||||
const additionalData = mock<IWorkflowExecuteAdditionalData>();
|
||||
const nodeExecuteFunctions = mock<INodeExecuteFunctions>();
|
||||
const triggerFunctions = mock<ITriggerFunctions>();
|
||||
nodeExecuteFunctions.getExecuteTriggerFunctions.mockReturnValue(triggerFunctions);
|
||||
const abortController = new AbortController();
|
||||
|
||||
test('should call closeFunction when manual trigger is aborted', async () => {
|
||||
const runPromise = workflow.runNode(
|
||||
executionData,
|
||||
runExecutionData,
|
||||
0,
|
||||
additionalData,
|
||||
nodeExecuteFunctions,
|
||||
'manual',
|
||||
abortController.signal,
|
||||
);
|
||||
// Yield back to the event-loop to let async parts of `runNode` execute
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
let isSettled = false;
|
||||
void runPromise.then(() => {
|
||||
isSettled = true;
|
||||
});
|
||||
expect(isSettled).toBe(false);
|
||||
expect(abortController.signal.aborted).toBe(false);
|
||||
expect(triggerResponse.closeFunction).not.toHaveBeenCalled();
|
||||
|
||||
abortController.abort();
|
||||
expect(triggerResponse.closeFunction).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue