Merge branch 'master' of github.com:n8n-io/n8n into seatable_node_rework2

This commit is contained in:
Jonathan Bennetts 2024-10-28 10:59:59 +00:00
commit 8ed9d1e90c
No known key found for this signature in database
4300 changed files with 270475 additions and 89492 deletions

View file

@ -9,7 +9,7 @@
"type=bind,source=${localEnv:HOME}/.n8n,target=/home/node/.n8n,consistency=cached" "type=bind,source=${localEnv:HOME}/.n8n,target=/home/node/.n8n,consistency=cached"
], ],
"forwardPorts": [8080, 5678], "forwardPorts": [8080, 5678],
"postCreateCommand": "corepack prepare --activate && pnpm install ", "postCreateCommand": "corepack prepare --activate && pnpm install",
"postAttachCommand": "pnpm build", "postAttachCommand": "pnpm build",
"customizations": { "customizations": {
"codespaces": { "codespaces": {

View file

@ -19,5 +19,6 @@ services:
- ..:/workspaces:cached - ..:/workspaces:cached
command: sleep infinity command: sleep infinity
environment: environment:
DB_POSTGRESDB_HOST: postgres
DB_TYPE: postgresdb DB_TYPE: postgresdb
DB_POSTGRESDB_PASSWORD: password DB_POSTGRESDB_PASSWORD: password

View file

@ -10,6 +10,7 @@ packages/**/.turbo
packages/**/*.test.* packages/**/*.test.*
.git .git
.github .github
!.github/scripts
*.tsbuildinfo *.tsbuildinfo
packages/cli/dist/**/e2e.* packages/cli/dist/**/e2e.*
docker/compose docker/compose

View file

@ -4,75 +4,79 @@ We have very precise rules over how Pull Requests (to the `master` branch) must
A PR title consists of these elements: A PR title consists of these elements:
``` ```text
<type>(<scope>): <summary> <type>(<scope>): <summary>
│ │ │ │ │ │
│ │ └─⫸ Summary: In imperative present tense. │ │ └─⫸ Summary: In imperative present tense.
| | Capitalized | | Capitalized
| | No period at the end. | | No period at the end.
│ │ │ │
│ └─⫸ Scope: API|core|editor|* Node │ └─⫸ Scope: API | benchmark | core | editor | * Node
└─⫸ Type: build|ci|docs|feat|fix|perf|refactor|test └─⫸ Type: build | ci | chore | docs | feat | fix | perf | refactor | test
``` ```
- PR title - PR title
- type - type
- scope (*optional*) - scope (_optional_)
- summary - summary
- PR description - PR description
- body (optional) - body (optional)
- blank line - blank line
- footer (optional) - footer (optional)
The structure looks like this: The structure looks like this:
### **Type** ## Type
Must be one of the following: Must be one of the following:
- `feat` - A new feature | type | description | appears in changelog |
- `fix` - A bug fix | --- | --- | --- |
- `perf` - A code change that improves performance | `feat` | A new feature | ✅ |
- `test` - Adding missing tests or correcting existing tests | `fix` | A bug fix | ✅ |
- `docs` - Documentation only changes | `perf` | A code change that improves performance | ✅ |
- `refactor` - A code change that neither fixes a bug nor adds a feature | `test` | Adding missing tests or correcting existing tests | ❌ |
- `build` - Changes that affect the build system or external dependencies (example scopes: broccoli, npm) | `docs` | Documentation only changes | ❌ |
- `ci` - Changes to our CI configuration files and scripts (e.g. Github actions) | `refactor` | A behavior-neutral code change that neither fixes a bug nor adds a feature | ❌ |
| `build` | Changes that affect the build system or external dependencies (TypeScript, Jest, pnpm, etc.) | ❌ |
| `ci` | Changes to CI configuration files and scripts (e.g. Github actions) | ❌ |
| `chore` | Routine tasks, maintenance, and minor updates not covered by other types | ❌ |
If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However if there is any BREAKING CHANGE (see Footer section below), the commit will always appear in the changelog. > BREAKING CHANGES (see Footer section below), will **always** appear in the changelog unless suffixed with `no-changelog`.
### **Scope (optional)** ## Scope (optional)
The scope should specify the place of the commit change as long as the commit clearly addresses one of the following supported scopes. (Otherwise, omit the scope!) The scope should specify the place of the commit change as long as the commit clearly addresses one of the following supported scopes. (Otherwise, omit the scope!)
- `API` - changes to the *public* API - `API` - changes to the _public_ API
- `benchmark` - changes to the benchmark cli
- `core` - changes to the core / private API / backend of n8n - `core` - changes to the core / private API / backend of n8n
- `editor` - changes to the Editor UI - `editor` - changes to the Editor UI
- `* Node` - changes to a specific node or trigger node (”`*`” to be replaced with the node name, not its display name), e.g. - `* Node` - changes to a specific node or trigger node (”`*`” to be replaced with the node name, not its display name), e.g.
- mattermost → Mattermost Node - mattermost → Mattermost Node
- microsoftToDo → Microsoft To Do Node - microsoftToDo → Microsoft To Do Node
- n8n → n8n Node - n8n → n8n Node
### **Summary** ## Summary
The summary contains succinct description of the change: The summary contains succinct description of the change:
- use the imperative, present tense: "change" not "changed" nor "changes" - use the imperative, present tense: "change" not "changed" nor "changes"
- capitalize the first letter - capitalize the first letter
- *no* dot (.) at the end - _no_ dot (.) at the end
- do *not* include Linear ticket IDs etc. (e.g. N8N-1234) - do _not_ include Linear ticket IDs etc. (e.g. N8N-1234)
- suffix with “(no-changelog)” for commits / PRs that should not get mentioned in the changelog. - suffix with “(no-changelog)” for commits / PRs that should not get mentioned in the changelog.
### **Body (optional)** ## Body (optional)
Just as in the **summary**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior. Just as in the **summary**, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.
### **Footer (optional)** ## Footer (optional)
The footer can contain information about breaking changes and deprecations and is also the place to [reference GitHub issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), Linear tickets, and other PRs that this commit closes or is related to. For example: The footer can contain information about breaking changes and deprecations and is also the place to [reference GitHub issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), Linear tickets, and other PRs that this commit closes or is related to. For example:
``` ```text
BREAKING CHANGE: <breaking change summary> BREAKING CHANGE: <breaking change summary>
<BLANK LINE> <BLANK LINE>
<breaking change description + migration instructions> <breaking change description + migration instructions>
@ -83,7 +87,7 @@ Fixes #<issue number>
or or
``` ```text
DEPRECATED: <what is deprecated> DEPRECATED: <what is deprecated>
<BLANK LINE> <BLANK LINE>
<deprecation description + recommended update path> <deprecation description + recommended update path>
@ -95,18 +99,18 @@ Closes #<pr number>
A Breaking Change section should start with the phrase "`BREAKING CHANGE:` " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions. A Breaking Change section should start with the phrase "`BREAKING CHANGE:` " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
> 💡 A breaking change can additionally also be marked by adding a “`!`” to the header, right before the “`:`”, e.g. `feat(editor)!: Remove support for dark mode` > 💡 A breaking change can additionally also be marked by adding a “`!`” to the header, right before the “`:`”, e.g. `feat(editor)!: Remove support for dark mode`
> >
> This makes locating breaking changes easier when just skimming through commit messages. > This makes locating breaking changes easier when just skimming through commit messages.
> 💡 The breaking changes must also be added to the [packages/cli/BREAKING-CHANGES.md](https://github.com/n8n-io/n8n/blob/master/packages/cli/BREAKING-CHANGES.md) file located in the n8n repository. > 💡 The breaking changes must also be added to the [packages/cli/BREAKING-CHANGES.md](https://github.com/n8n-io/n8n/blob/master/packages/cli/BREAKING-CHANGES.md) file located in the n8n repository.
Similarly, a Deprecation section should start with "`DEPRECATED:` " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path. Similarly, a Deprecation section should start with "`DEPRECATED:` " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
### **Revert commits** ### Revert commits
If the commit reverts a previous commit, it should begin with `revert:` , followed by the header of the reverted commit. If the commit reverts a previous commit, it should begin with `revert:` , followed by the header of the reverted commit.
The content of the commit message body should contain: The content of the commit message body should contain:
- information about the SHA of the commit being reverted in the following format: `This reverts commit <SHA>`, - information about the SHA of the commit being reverted in the following format: `This reverts commit <SHA>`,
- a clear description of the reason for reverting the commit message. - a clear description of the reason for reverting the commit message.

View 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');
}

View file

@ -5,7 +5,7 @@
"debug": "4.3.4", "debug": "4.3.4",
"glob": "10.3.10", "glob": "10.3.10",
"p-limit": "3.1.0", "p-limit": "3.1.0",
"picocolors": "1.0.0", "picocolors": "1.0.1",
"semver": "7.5.4", "semver": "7.5.4",
"tempfile": "5.0.0", "tempfile": "5.0.0",
"typescript": "*" "typescript": "*"

View file

@ -1,12 +1,15 @@
const { writeFileSync } = require('fs'); const { writeFileSync } = require('fs');
const { resolve } = require('path'); const { resolve } = require('path');
const baseDir = resolve(__dirname, '..'); const baseDir = resolve(__dirname, '../..');
const trimPackageJson = (packageName) => { const trimPackageJson = (packageName) => {
const filePath = resolve(baseDir, 'packages', packageName, 'package.json'); const filePath = resolve(baseDir, 'packages', packageName, 'package.json');
const { scripts, peerDependencies, devDependencies, dependencies, ...packageJson } = require( const { scripts, peerDependencies, devDependencies, dependencies, ...packageJson } = require(
filePath, filePath,
); );
if (packageName === '@n8n/chat') {
packageJson.dependencies = dependencies;
}
writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8'); writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n', 'utf-8');
}; };

View file

@ -16,7 +16,11 @@ const changelogStream = conventionalChangelog({
releaseCount: 1, releaseCount: 1,
tagPrefix: 'n8n@', tagPrefix: 'n8n@',
transform: (commit, callback) => { transform: (commit, callback) => {
callback(null, commit.header.includes('(no-changelog)') ? undefined : commit); const hasNoChangelogInHeader = commit.header.includes('(no-changelog)');
const isBenchmarkScope = commit.scope === 'benchmark';
// Ignore commits that have 'benchmark' scope or '(no-changelog)' in the header
callback(null, hasNoChangelogInHeader || isBenchmarkScope ? undefined : commit);
}, },
}).on('error', (err) => { }).on('error', (err) => {
console.error(err.stack); console.error(err.stack);

View file

@ -0,0 +1,43 @@
name: Destroy Benchmark Env
on:
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
permissions:
id-token: write
contents: read
concurrency:
group: benchmark
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
environment: benchmarking
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- name: Azure login
uses: azure/login@v2.1.1
with:
client-id: ${{ secrets.BENCHMARK_ARM_CLIENT_ID }}
tenant-id: ${{ secrets.BENCHMARK_ARM_TENANT_ID }}
subscription-id: ${{ secrets.BENCHMARK_ARM_SUBSCRIPTION_ID }}
- run: corepack enable
- uses: actions/setup-node@v4.0.2
with:
node-version: 20.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Destroy cloud env
run: pnpm destroy-cloud-env
working-directory: packages/@n8n/benchmark

101
.github/workflows/benchmark-nightly.yml vendored Normal file
View file

@ -0,0 +1,101 @@
name: Run Nightly Benchmark
run-name: Benchmark ${{ inputs.n8n_tag || 'nightly' }}
on:
schedule:
- cron: '30 1,2,3 * * *'
workflow_dispatch:
inputs:
debug:
description: 'Use debug logging'
required: true
default: 'false'
n8n_tag:
description: 'Name of the n8n docker tag to run the benchmark against.'
required: true
default: 'nightly'
benchmark_tag:
description: 'Name of the benchmark cli docker tag to run the benchmark with.'
required: true
default: 'latest'
env:
ARM_CLIENT_ID: ${{ secrets.BENCHMARK_ARM_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.BENCHMARK_ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.BENCHMARK_ARM_TENANT_ID }}
N8N_TAG: ${{ inputs.n8n_tag || 'nightly' }}
N8N_BENCHMARK_TAG: ${{ inputs.benchmark_tag || 'latest' }}
DEBUG: ${{ inputs.debug == 'true' && '--debug' || '' }}
permissions:
id-token: write
contents: read
concurrency:
group: benchmark
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
environment: benchmarking
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: '1.8.5'
- run: corepack enable
- uses: actions/setup-node@v4.0.2
with:
node-version: 20.x
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Azure login
uses: azure/login@v2.1.1
with:
client-id: ${{ env.ARM_CLIENT_ID }}
tenant-id: ${{ env.ARM_TENANT_ID }}
subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }}
- name: Destroy any existing environment
run: pnpm destroy-cloud-env
working-directory: packages/@n8n/benchmark
- name: Provision the environment
run: pnpm provision-cloud-env ${{ env.DEBUG }}
working-directory: packages/@n8n/benchmark
- name: Run the benchmark
env:
BENCHMARK_RESULT_WEBHOOK_URL: ${{ secrets.BENCHMARK_RESULT_WEBHOOK_URL }}
BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER: ${{ secrets.BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER }}
N8N_LICENSE_CERT: ${{ secrets.N8N_BENCHMARK_LICENSE_CERT }}
run: |
pnpm benchmark-in-cloud \
--vus 5 \
--duration 1m \
--n8nTag ${{ env.N8N_TAG }} \
--benchmarkTag ${{ env.N8N_BENCHMARK_TAG }} \
${{ env.DEBUG }}
working-directory: packages/@n8n/benchmark
# We need to login again because the access token expires
- name: Azure login
if: always()
uses: azure/login@v2.1.1
with:
client-id: ${{ env.ARM_CLIENT_ID }}
tenant-id: ${{ env.ARM_TENANT_ID }}
subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }}
- name: Destroy the environment
if: always()
run: pnpm destroy-cloud-env ${{ env.DEBUG }}
working-directory: packages/@n8n/benchmark

View file

@ -26,7 +26,7 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Build relevant packages - name: Build relevant packages
run: pnpm --filter @n8n/client-oauth2 --filter @n8n/imap --filter n8n-workflow --filter n8n-core --filter n8n-nodes-base --filter @n8n/n8n-nodes-langchain build run: pnpm build:nodes
- run: npm install --prefix=.github/scripts --no-package-lock - run: npm install --prefix=.github/scripts --no-package-lock

View file

@ -7,8 +7,7 @@ on:
- edited - edited
- synchronize - synchronize
branches: branches:
- '**' - 'master'
- '!release/*'
jobs: jobs:
check-pr-title: check-pr-title:
@ -29,6 +28,6 @@ jobs:
- name: Validate PR title - name: Validate PR title
id: validate_pr_title id: validate_pr_title
uses: n8n-io/validate-n8n-pull-request-title@v2.0.1 uses: n8n-io/validate-n8n-pull-request-title@v2.2.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -4,19 +4,49 @@ on:
workflow_dispatch: workflow_dispatch:
pull_request_review: pull_request_review:
types: [submitted] types: [submitted]
branch:
- 'master'
paths:
- packages/design-system/**
- .github/workflows/chromatic.yml
concurrency: concurrency:
group: chromatic-${{ github.event.pull_request.number || github.ref }} group: chromatic-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
get-metadata:
name: Get Metadata
runs-on: ubuntu-latest
steps:
- name: Check out current commit
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
- name: Determine changed files
uses: tomi/paths-filter-action@v3.0.2
id: changed
if: github.event_name == 'pull_request_review'
with:
filters: |
design_system:
- packages/design-system/**
- .github/workflows/chromatic.yml
outputs:
design_system_files_changed: ${{ steps.changed.outputs.design_system == 'true' }}
is_community_pr: ${{ contains(github.event.pull_request.labels.*.name, 'community') }}
is_pr_target_master: ${{ github.event.pull_request.base.ref == 'master' }}
is_dispatch: ${{ github.event_name == 'workflow_dispatch' }}
is_pr_approved: ${{ github.event.review.state == 'approved' }}
chromatic: chromatic:
if: ${{ github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'community') }} needs: [get-metadata]
if: |
needs.get-metadata.outputs.is_dispatch == 'true' ||
(
needs.get-metadata.outputs.design_system_files_changed == 'true' &&
needs.get-metadata.outputs.is_community_pr == 'false' &&
needs.get-metadata.outputs.is_pr_target_master == 'true' &&
needs.get-metadata.outputs.is_pr_approved == 'true'
)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1

View file

@ -23,6 +23,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build - name: Build
run: pnpm build run: pnpm build
@ -38,12 +41,14 @@ jobs:
needs: install-and-build needs: install-and-build
strategy: strategy:
matrix: matrix:
node-version: [18.x, 20.x, 22.x] node-version: [18.x, 20.x, 22.4]
with: with:
ref: ${{ inputs.branch }} ref: ${{ inputs.branch }}
nodeVersion: ${{ matrix.node-version }} nodeVersion: ${{ matrix.node-version }}
cacheKey: ${{ github.sha }}-base:build cacheKey: ${{ github.sha }}-base:build
collectCoverage: true collectCoverage: ${{ matrix.node-version == '20.x' }}
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
lint: lint:
name: Lint name: Lint

View file

@ -8,6 +8,8 @@ on:
paths: paths:
- packages/cli/src/databases/** - packages/cli/src/databases/**
- .github/workflows/ci-postgres-mysql.yml - .github/workflows/ci-postgres-mysql.yml
pull_request_review:
types: [submitted]
concurrency: concurrency:
group: db-${{ github.event.pull_request.number || github.ref }} group: db-${{ github.event.pull_request.number || github.ref }}
@ -17,6 +19,7 @@ jobs:
build: build:
name: Install & Build name: Install & Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request_review' || startsWith(github.event.pull_request.base.ref, 'release/')
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
- run: corepack enable - run: corepack enable
@ -26,6 +29,9 @@ jobs:
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build Backend - name: Build Backend
run: pnpm build:backend run: pnpm build:backend
@ -52,6 +58,9 @@ jobs:
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Restore cached build artifacts - name: Restore cached build artifacts
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.0
with: with:
@ -60,7 +69,7 @@ jobs:
- name: Test SQLite Pooled - name: Test SQLite Pooled
working-directory: packages/cli working-directory: packages/cli
run: pnpm jest --coverage run: pnpm jest
mysql: mysql:
name: MySQL name: MySQL
@ -78,6 +87,9 @@ jobs:
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Restore cached build artifacts - name: Restore cached build artifacts
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.0
with: with:
@ -112,6 +124,9 @@ jobs:
cache: 'pnpm' cache: 'pnpm'
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Restore cached build artifacts - name: Restore cached build artifacts
uses: actions/cache/restore@v4.0.0 uses: actions/cache/restore@v4.0.0
with: with:

View file

@ -1,6 +1,10 @@
name: Build, unit test and lint branch name: Build, unit test and lint branch
on: [pull_request] on:
pull_request:
branches:
- '**'
- '!release/*'
jobs: jobs:
install-and-build: install-and-build:
@ -9,7 +13,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
repository: n8n-io/n8n
ref: refs/pull/${{ github.event.pull_request.number }}/merge ref: refs/pull/${{ github.event.pull_request.number }}/merge
- run: corepack enable - run: corepack enable
@ -21,9 +24,15 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build - name: Build
run: pnpm build run: pnpm build
- name: Run formatcheck
run: pnpm format:check
- name: Run typecheck - name: Run typecheck
run: pnpm typecheck run: pnpm typecheck

View file

@ -0,0 +1,43 @@
name: Benchmark Docker Image CI
on:
workflow_dispatch:
push:
branches:
- master
paths:
- 'packages/@n8n/benchmark/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.github/workflows/docker-images-benchmark.yml'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build
uses: docker/build-push-action@v5.1.0
with:
context: .
file: ./packages/@n8n/benchmark/Dockerfile
platforms: linux/amd64
provenance: false
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/n8n-benchmark:latest

View file

@ -6,10 +6,6 @@ on:
- cron: '0 1 * * *' - cron: '0 1 * * *'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
repository:
description: 'GitHub repository to create image off.'
required: true
default: 'n8n-io/n8n'
branch: branch:
description: 'GitHub branch to create image off.' description: 'GitHub branch to create image off.'
required: true required: true
@ -36,6 +32,9 @@ on:
required: false required: false
default: '' default: ''
env:
N8N_TAG: ${{ inputs.tag || 'nightly' }}
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -49,7 +48,6 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
with: with:
repository: ${{ github.event.inputs.repository || 'n8n-io/n8n' }}
ref: ${{ github.event.inputs.branch || 'master' }} ref: ${{ github.event.inputs.branch || 'master' }}
- name: Set up QEMU - name: Set up QEMU
@ -69,7 +67,7 @@ jobs:
[[ "${{github.event.inputs.merge-master}}" == "true" ]] && git remote add upstream https://github.com/n8n-io/n8n.git -f; git merge upstream/master --allow-unrelated-histories || echo "" [[ "${{github.event.inputs.merge-master}}" == "true" ]] && git remote add upstream https://github.com/n8n-io/n8n.git -f; git merge upstream/master --allow-unrelated-histories || echo ""
shell: bash shell: bash
- name: Build and push - name: Build and push to DockerHub
uses: docker/build-push-action@v5.1.0 uses: docker/build-push-action@v5.1.0
with: with:
context: . context: .
@ -81,7 +79,22 @@ jobs:
push: true push: true
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.tag || 'nightly' }} tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ env.N8N_TAG }}
- name: Login to GitHub Container Registry
if: env.N8N_TAG == 'nightly'
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push image to GHCR
if: env.N8N_TAG == 'nightly'
run: |
docker buildx imagetools create \
--tag ghcr.io/${{ github.repository_owner }}/n8n:nightly \
${{ secrets.DOCKER_USERNAME }}/n8n:nightly
- name: Call Success URL - optionally - name: Call Success URL - optionally
run: | run: |

View file

@ -1,48 +0,0 @@
name: Docker Image CI
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
- name: Get the version
id: vars
run: echo ::set-output name=tag::$(echo ${GITHUB_REF:14})
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build
uses: docker/build-push-action@v5.1.0
with:
context: ./docker/images/n8n
build-args: |
N8N_VERSION=${{ steps.vars.outputs.tag }}
platforms: linux/amd64,linux/arm64
provenance: false
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/n8n:${{ steps.vars.outputs.tag }}
ghcr.io/${{ github.repository_owner }}/n8n:${{ steps.vars.outputs.tag }}

View file

@ -22,11 +22,6 @@ on:
required: false required: false
default: 'browsers:node18.12.0-chrome107' default: 'browsers:node18.12.0-chrome107'
type: string type: string
cache-key:
description: 'Cache key for modules and build artifacts.'
required: false
default: ${{ github.sha }}-${{ inputs.run-env }}-e2e-modules
type: string
record: record:
description: 'Record test run.' description: 'Record test run.'
required: false required: false
@ -46,6 +41,11 @@ on:
description: 'PR number to run tests for.' description: 'PR number to run tests for.'
required: false required: false
type: number type: number
node_view_version:
description: 'Node View version to run tests with.'
required: false
default: '1'
type: string
secrets: secrets:
CYPRESS_RECORD_KEY: CYPRESS_RECORD_KEY:
description: 'Cypress record key.' description: 'Cypress record key.'
@ -78,7 +78,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
repository: n8n-io/n8n
ref: ${{ inputs.branch }} ref: ${{ inputs.branch }}
- name: Checkout PR - name: Checkout PR
@ -99,8 +98,6 @@ jobs:
runTests: false runTests: false
install: false install: false
build: pnpm build build: pnpm build
env:
VUE_APP_MAX_PINNED_DATA_SIZE: 16384
- name: Cypress install - name: Cypress install
working-directory: cypress working-directory: cypress
@ -113,7 +110,7 @@ jobs:
/github/home/.cache /github/home/.cache
/github/home/.pnpm-store /github/home/.pnpm-store
./packages/**/dist ./packages/**/dist
key: ${{ inputs.cache-key }} key: ${{ github.sha }}-e2e
testing: testing:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -130,7 +127,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
repository: n8n-io/n8n
ref: ${{ inputs.branch }} ref: ${{ inputs.branch }}
- name: Checkout PR - name: Checkout PR
@ -148,7 +144,7 @@ jobs:
/github/home/.cache /github/home/.cache
/github/home/.pnpm-store /github/home/.pnpm-store
./packages/**/dist ./packages/**/dist
key: ${{ inputs.cache-key }} key: ${{ github.sha }}-e2e
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
@ -169,6 +165,7 @@ jobs:
spec: '${{ inputs.spec }}' spec: '${{ inputs.spec }}'
env: env:
NODE_OPTIONS: --dns-result-order=ipv4first NODE_OPTIONS: --dns-result-order=ipv4first
CYPRESS_NODE_VIEW_VERSION: ${{ inputs.node_view_version }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
E2E_TESTS: true E2E_TESTS: true

View file

@ -3,33 +3,65 @@ name: PR E2E
on: on:
pull_request_review: pull_request_review:
types: [submitted] types: [submitted]
branch:
- 'master'
concurrency: concurrency:
group: e2e-${{ github.event.pull_request.number || github.ref }} group: e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
get-metadata:
name: Get Metadata
runs-on: ubuntu-latest
steps:
- name: Check out current commit
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 2
- name: Determine changed files
uses: tomi/paths-filter-action@v3.0.2
id: changed
with:
filters: |
not_ignored:
- '!.devcontainer/**'
- '!.github/*'
- '!.github/scripts/*'
- '!.github/workflows/benchmark-*'
- '!.github/workflows/check-*'
- '!.vscode/**'
- '!docker/**'
- '!packages/@n8n/benchmark/**'
- '!**/*.md'
predicate-quantifier: 'every'
outputs:
# The workflow should run when:
# - It has changes to files that are not ignored
# - It is not a community PR
# - It is targeting master or a release branch
should_run: ${{ steps.changed.outputs.not_ignored == 'true' && !contains(github.event.pull_request.labels.*.name, 'community') && (github.event.pull_request.base.ref == 'master' || startsWith(github.event.pull_request.base.ref, 'release/')) }}
run-e2e-tests: run-e2e-tests:
name: E2E [Electron/Node 18] name: E2E [Electron/Node 18]
uses: ./.github/workflows/e2e-reusable.yml uses: ./.github/workflows/e2e-reusable.yml
if: ${{ github.event.review.state == 'approved' && !contains(github.event.pull_request.labels.*.name, 'community') }} needs: [get-metadata]
if: ${{ github.event.review.state == 'approved' && needs.get-metadata.outputs.should_run == 'true' }}
with: with:
pr_number: ${{ github.event.pull_request.number }} pr_number: ${{ github.event.pull_request.number }}
user: ${{ github.event.pull_request.user.login || 'PR User' }} user: ${{ github.event.pull_request.user.login || 'PR User' }}
spec: 'e2e/*'
secrets: secrets:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
post-e2e-tests: post-e2e-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: E2E [Electron/Node 18] - Checks name: E2E [Electron/Node 18] - Checks
needs: [run-e2e-tests] needs: [get-metadata, run-e2e-tests]
if: always() if: always()
steps: steps:
- name: E2E success comment - name: E2E success comment
if: ${{!contains(github.event.pull_request.labels.*.name, 'community') && needs.run-e2e-tests.outputs.tests_passed == 'true' }} if: ${{ needs.get-metadata.outputs.should_run == 'true' && needs.run-e2e-tests.outputs.tests_passed == 'true' }}
uses: peter-evans/create-or-update-comment@v4.0.0 uses: peter-evans/create-or-update-comment@v4.0.0
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}

View file

@ -27,6 +27,11 @@ on:
description: 'URL to call after workflow is done.' description: 'URL to call after workflow is done.'
required: false required: false
default: '' default: ''
node_view_version:
description: 'Node View version to run tests with.'
required: false
default: '1'
type: string
jobs: jobs:
calls-start-url: calls-start-url:
@ -46,6 +51,7 @@ jobs:
branch: ${{ github.event.inputs.branch || 'master' }} branch: ${{ github.event.inputs.branch || 'master' }}
user: ${{ github.event.inputs.user || 'PR User' }} user: ${{ github.event.inputs.user || 'PR User' }}
spec: ${{ github.event.inputs.spec || 'e2e/*' }} spec: ${{ github.event.inputs.spec || 'e2e/*' }}
node_view_version: ${{ github.event.inputs.node_view_version || '1' }}
secrets: secrets:
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}

View file

@ -21,7 +21,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
repository: n8n-io/n8n
ref: ${{ inputs.ref }} ref: ${{ inputs.ref }}
- run: corepack enable - run: corepack enable
@ -33,6 +32,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Setup build cache
uses: rharkor/caching-for-turbo@v1.5
- name: Build - name: Build
if: ${{ inputs.cacheKey == '' }} if: ${{ inputs.cacheKey == '' }}
run: pnpm build run: pnpm build

View file

@ -16,6 +16,7 @@ jobs:
(github.event_name == 'pull_request' && github.event.pull_request.merged == false && github.event.action == 'closed') (github.event_name == 'pull_request' && github.event.pull_request.merged == false && github.event.action == 'closed')
steps: steps:
- uses: fjogeleit/http-request-action@dea46570591713c7de04a5b556bf2ff7bdf0aa9c # v1 - uses: fjogeleit/http-request-action@dea46570591713c7de04a5b556bf2ff7bdf0aa9c # v1
if: ${{!contains(github.event.pull_request.labels.*.name, 'community')}}
name: Notify name: Notify
env: env:
PR_URL: ${{ github.event.pull_request.html_url }} PR_URL: ${{ github.event.pull_request.html_url }}

View file

@ -56,12 +56,12 @@ jobs:
git push -f origin refs/remotes/origin/${{ github.event.inputs.base-branch }}:refs/heads/release/${{ env.NEXT_RELEASE }} git push -f origin refs/remotes/origin/${{ github.event.inputs.base-branch }}:refs/heads/release/${{ env.NEXT_RELEASE }}
- name: Push the release branch, and Create the PR - name: Push the release branch, and Create the PR
uses: peter-evans/create-pull-request@v5 uses: peter-evans/create-pull-request@v6
with: with:
base: 'release/${{ env.NEXT_RELEASE }}' base: 'release/${{ env.NEXT_RELEASE }}'
branch: '${{ env.NEXT_RELEASE }}-pr' branch: 'release-pr/${{ env.NEXT_RELEASE }}'
commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}' commit-message: ':rocket: Release ${{ env.NEXT_RELEASE }}'
delete-branch: true delete-branch: true
labels: 'release' labels: release,release:${{ github.event.inputs.release-type }}
title: ':rocket: Release ${{ env.NEXT_RELEASE }}' title: ':rocket: Release ${{ env.NEXT_RELEASE }}'
body-path: 'CHANGELOG-${{ env.NEXT_RELEASE }}.md' body-path: 'CHANGELOG-${{ env.NEXT_RELEASE }}.md'

View file

@ -8,18 +8,17 @@ on:
- 'release/*' - 'release/*'
jobs: jobs:
publish-release: publish-to-npm:
if: github.event.pull_request.merged == true name: Publish to NPM
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
timeout-minutes: 10
permissions: permissions:
contents: write
id-token: write id-token: write
timeout-minutes: 60
env: env:
NPM_CONFIG_PROVENANCE: true NPM_CONFIG_PROVENANCE: true
outputs:
release: ${{ steps.set-release.outputs.release }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
@ -45,30 +44,103 @@ jobs:
- name: Publish to NPM - name: Publish to NPM
run: | run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc 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 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 pnpm publish -r --publish-branch ${{github.event.pull_request.base.ref}} --access public --tag rc --no-git-checks
npm dist-tag rm n8n rc npm dist-tag rm n8n rc
- id: set-release
run: echo "release=${{ env.RELEASE }}" >> $GITHUB_OUTPUT
publish-to-docker-hub:
name: Publish to DockerHub
needs: [publish-to-npm]
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@v3.0.0
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build
uses: docker/build-push-action@v5.1.0
with:
context: ./docker/images/n8n
build-args: |
N8N_VERSION=${{ needs.publish-to-npm.outputs.release }}
platforms: linux/amd64,linux/arm64
provenance: false
push: true
tags: |
${{ secrets.DOCKER_USERNAME }}/n8n:${{ needs.publish-to-npm.outputs.release }}
ghcr.io/${{ github.repository_owner }}/n8n:${{ needs.publish-to-npm.outputs.release }}
create-github-release:
name: Create a GitHub Release
needs: [publish-to-npm, publish-to-docker-hub]
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true
timeout-minutes: 5
permissions:
contents: write
id-token: write
steps:
- name: Create a Release on GitHub - name: Create a Release on GitHub
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
commit: ${{github.event.pull_request.base.ref}} commit: ${{github.event.pull_request.base.ref}}
tag: 'n8n@${{env.RELEASE}}' tag: 'n8n@${{ needs.publish-to-npm.outputs.release }}'
prerelease: true prerelease: true
makeLatest: false makeLatest: false
body: ${{github.event.pull_request.body}} body: ${{github.event.pull_request.body}}
trigger-release-note:
name: Trigger a release note
needs: [publish-to-npm, create-github-release]
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Trigger a release note - name: Trigger a release note
continue-on-error: true run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{ needs.publish-to-npm.outputs.release }}"}'
run: curl -u docsWorkflows:${{ secrets.N8N_WEBHOOK_DOCS_PASSWORD }} --request GET 'https://internal.users.n8n.cloud/webhook/trigger-release-note' --header 'Content-Type:application/json' --data '{"version":"${{env.RELEASE}}"}'
# - name: Merge Release into 'master' # merge-back-into-master:
# run: | # name: Merge back into master
# git fetch origin # needs: [publish-to-npm, create-github-release]
# git checkout --track origin/master # if: ${{ github.event.pull_request.merged == true && !contains(github.event.pull_request.labels.*.name, 'release:patch') }}
# git config user.name "Jan Oberhauser" # runs-on: ubuntu-latest
# git config user.email jan.oberhauser@gmail.com # steps:
# git merge --ff n8n@${{env.RELEASE}} # - uses: actions/checkout@v4.1.1
# git push origin master # with:
# git push origin :${{github.event.pull_request.base.ref}} # fetch-depth: 0
# - run: |
# git checkout --track origin/master
# git config user.name "github-actions[bot]"
# git config user.email 41898282+github-actions[bot]@users.noreply.github.com
# git merge --ff n8n@${{ needs.publish-to-npm.outputs.release }}
# git push origin master
# git push origin :${{github.event.pull_request.base.ref}}

View file

@ -73,6 +73,7 @@ jobs:
env: env:
N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}} N8N_ENCRYPTION_KEY: ${{secrets.ENCRYPTION_KEY}}
SKIP_STATISTICS_EVENTS: true SKIP_STATISTICS_EVENTS: true
DB_SQLITE_POOL_SIZE: 4
# - # -
# name: Export credentials # name: Export credentials
# if: always() # if: always()

View file

@ -22,6 +22,10 @@ on:
required: false required: false
default: false default: false
type: boolean type: boolean
secrets:
CODECOV_TOKEN:
description: 'Codecov upload token.'
required: false
jobs: jobs:
unit-test: unit-test:
@ -32,7 +36,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4.1.1 - uses: actions/checkout@v4.1.1
with: with:
repository: n8n-io/n8n
ref: ${{ inputs.ref }} ref: ${{ inputs.ref }}
- run: corepack enable - run: corepack enable
@ -45,6 +48,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Setup build cache
if: inputs.collectCoverage != true
uses: rharkor/caching-for-turbo@v1.5
- name: Build - name: Build
if: ${{ inputs.cacheKey == '' }} if: ${{ inputs.cacheKey == '' }}
run: pnpm build run: pnpm build
@ -67,6 +74,6 @@ jobs:
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: inputs.collectCoverage if: inputs.collectCoverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4.5.0
with: with:
files: packages/@n8n/chat/coverage/cobertura-coverage.xml,packages/@n8n/nodes-langchain/coverage/cobertura-coverage.xml,packages/@n8n/permissions/coverage/cobertura-coverage.xml,packages/@n8n/client-oauth2/coverage/cobertura-coverage.xml,packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/@n8n/codemirror-lang/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml token: ${{ secrets.CODECOV_TOKEN }}

View file

@ -7,3 +7,11 @@ packages/nodes-base/nodes/**/test
packages/cli/templates/form-trigger.handlebars packages/cli/templates/form-trigger.handlebars
cypress/fixtures cypress/fixtures
CHANGELOG.md CHANGELOG.md
.github/pull_request_template.md
# Ignored for now
**/*.md
# Handled by biome
**/*.ts
**/*.js
**/*.json
**/*.jsonc

View file

@ -1,10 +1,12 @@
{ {
"recommendations": [ "recommendations": [
"biomejs.biome",
"streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker",
"dangmai.workspace-default-settings", "dangmai.workspace-default-settings",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig", "EditorConfig.EditorConfig",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"mjmlio.vscode-mjml",
"Vue.volar" "Vue.volar"
] ]
} }

View file

@ -1,6 +1,22 @@
{ {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit",
"source.organizeImports.biome": "never"
},
"search.exclude": { "search.exclude": {
"node_modules": true, "node_modules": true,
"dist": true, "dist": true,

View file

@ -1,3 +1,702 @@
# [1.65.0](https://github.com/n8n-io/n8n/compare/n8n@1.64.0...n8n@1.65.0) (2024-10-24)
### Bug Fixes
* **AI Agent Node:** Preserve `intermediateSteps` when using output parser with non-tool agent ([#11363](https://github.com/n8n-io/n8n/issues/11363)) ([e61a853](https://github.com/n8n-io/n8n/commit/e61a8535aa39653b9a87575ea911a65318282167))
* **API:** `PUT /credentials/:id` should move the specified credential, not the first one in the database ([#11365](https://github.com/n8n-io/n8n/issues/11365)) ([e6b2f8e](https://github.com/n8n-io/n8n/commit/e6b2f8e7e6ebbb6e3776a976297d519e99ac6c64))
* **API:** Correct credential schema for response in `POST /credentials` ([#11340](https://github.com/n8n-io/n8n/issues/11340)) ([f495875](https://github.com/n8n-io/n8n/commit/f4958756b4976e0b608b9155dab84564f7e8804e))
* **core:** Account for waiting jobs during shutdown ([#11338](https://github.com/n8n-io/n8n/issues/11338)) ([c863abd](https://github.com/n8n-io/n8n/commit/c863abd08300b53ea898fc4d06aae97dec7afa9b))
* **core:** Add missing primary key to execution annotation tags table ([#11168](https://github.com/n8n-io/n8n/issues/11168)) ([b4b543d](https://github.com/n8n-io/n8n/commit/b4b543d41daa07753eca24ab93bf7445f672361d))
* **core:** Change dedupe value column type from varchar(255) to text ([#11357](https://github.com/n8n-io/n8n/issues/11357)) ([7a71cff](https://github.com/n8n-io/n8n/commit/7a71cff4d75fe4e7282a398b4843428e0161ba8c))
* **core:** Do not debounce webhooks, triggers and pollers activation ([#11306](https://github.com/n8n-io/n8n/issues/11306)) ([64bddf8](https://github.com/n8n-io/n8n/commit/64bddf86536ddd688638a643d24f80c947a12f31))
* **core:** Enforce nodejs version consistently ([#11323](https://github.com/n8n-io/n8n/issues/11323)) ([0fa2e8c](https://github.com/n8n-io/n8n/commit/0fa2e8ca85005362d9043d82469f3c3525f4c4ef))
* **core:** Fix memory issue with empty model response ([#11300](https://github.com/n8n-io/n8n/issues/11300)) ([216b119](https://github.com/n8n-io/n8n/commit/216b119350949de70f15cf2d61f474770803ad7a))
* **core:** Fix race condition when resolving post-execute promise ([#11360](https://github.com/n8n-io/n8n/issues/11360)) ([4f1816e](https://github.com/n8n-io/n8n/commit/4f1816e03db00219bc2e723e3048848aef7f8fe1))
* **core:** Sanitise IdP provided information in SAML test pages ([#11171](https://github.com/n8n-io/n8n/issues/11171)) ([74fc388](https://github.com/n8n-io/n8n/commit/74fc3889b946e8f224e65ef8d3d44125404aa4fc))
* Don't show pin button in input panel when there's binary data ([#11267](https://github.com/n8n-io/n8n/issues/11267)) ([c0b5b92](https://github.com/n8n-io/n8n/commit/c0b5b92f62a2d7ba60492eb27daced268b654fe9))
* **editor:** Add Personal project to main navigation ([#11161](https://github.com/n8n-io/n8n/issues/11161)) ([1f441f9](https://github.com/n8n-io/n8n/commit/1f441f97528f58e905eaf8930577bbcd08debf06))
* **editor:** Fix Cannot read properties of undefined (reading 'finished') ([#11367](https://github.com/n8n-io/n8n/issues/11367)) ([475d72e](https://github.com/n8n-io/n8n/commit/475d72e0bc9e13c6dc56129902f6f89c67547f78))
* **editor:** Fix delete all existing executions ([#11352](https://github.com/n8n-io/n8n/issues/11352)) ([3ec103f](https://github.com/n8n-io/n8n/commit/3ec103f8baaa89e579844947d945f00bec9e498e))
* **editor:** Fix pin data button disappearing after reload ([#11198](https://github.com/n8n-io/n8n/issues/11198)) ([3b2f63e](https://github.com/n8n-io/n8n/commit/3b2f63e248cd0cba04087e2f40e13d670073707d))
* **editor:** Fix RunData non-binary pagination when binary data is present ([#11309](https://github.com/n8n-io/n8n/issues/11309)) ([901888d](https://github.com/n8n-io/n8n/commit/901888d5b1027098653540c72f787f176941f35a))
* **editor:** Fix sorting problem in older browsers that don't support `toSorted` ([#11204](https://github.com/n8n-io/n8n/issues/11204)) ([c728a2f](https://github.com/n8n-io/n8n/commit/c728a2ffe01f510a237979a54897c4680a407800))
* **editor:** Follow-up fixes to projects side menu ([#11327](https://github.com/n8n-io/n8n/issues/11327)) ([4dde772](https://github.com/n8n-io/n8n/commit/4dde772814c55e66efcc9b369ae443328af21b14))
* **editor:** Keep always focus on the first item on the node's search panel ([#11193](https://github.com/n8n-io/n8n/issues/11193)) ([c57cac9](https://github.com/n8n-io/n8n/commit/c57cac9e4d447c3a4240a565f9f2de8aa3b7c513))
* **editor:** Open Community+ enrollment modal only for the instance owner ([#11292](https://github.com/n8n-io/n8n/issues/11292)) ([76724c3](https://github.com/n8n-io/n8n/commit/76724c3be6e001792433045c2b2aac0ef16d4b8a))
* **editor:** Record sessionStarted telemetry event in Setting Store ([#11334](https://github.com/n8n-io/n8n/issues/11334)) ([1b734dd](https://github.com/n8n-io/n8n/commit/1b734dd9f42885594ce02400cfb395a4f5e7e088))
* Ensure NDV params don't get cut off early and scrolled to the top ([#11252](https://github.com/n8n-io/n8n/issues/11252)) ([054fe97](https://github.com/n8n-io/n8n/commit/054fe9745ff6864f9088aa4cd66ed9e7869520d5))
* **HTTP Request Tool Node:** Fix the undefined response issue when authentication is enabled ([#11343](https://github.com/n8n-io/n8n/issues/11343)) ([094ec68](https://github.com/n8n-io/n8n/commit/094ec68d4c00848013aa4eec4ac5efbd2c92afc5))
* Include error in the message in JS task runner sandbox ([#11359](https://github.com/n8n-io/n8n/issues/11359)) ([0708b3a](https://github.com/n8n-io/n8n/commit/0708b3a1f8097af829c92fe106ea6ba375d6c500))
* **Microsoft SQL Node:** Fix execute query to allow for non select query to run ([#11335](https://github.com/n8n-io/n8n/issues/11335)) ([ba158b4](https://github.com/n8n-io/n8n/commit/ba158b4f8533bd3430db8766d4921f75db5c1a11))
* **OpenAI Chat Model Node, Ollama Chat Model Node:** Change default model to a more up-to-date option ([#11293](https://github.com/n8n-io/n8n/issues/11293)) ([0be04c6](https://github.com/n8n-io/n8n/commit/0be04c6348d8c059a96c3d37a6d6cd587bfb97f3))
* **Pinecone Vector Store Node:** Prevent populating of vectors after manually stopping the execution ([#11288](https://github.com/n8n-io/n8n/issues/11288)) ([fbae17d](https://github.com/n8n-io/n8n/commit/fbae17d8fb35a5197fa183e3639bb36762dc73d2))
* **Postgres Node:** Special datetime values cause errors ([#11225](https://github.com/n8n-io/n8n/issues/11225)) ([3c57f46](https://github.com/n8n-io/n8n/commit/3c57f46aaeb968d2974f2dc9790317a6a6fab624))
* Resend invite operation on users list ([#11351](https://github.com/n8n-io/n8n/issues/11351)) ([e4218de](https://github.com/n8n-io/n8n/commit/e4218debd18812fa3aa508339afd3de03c4d69dc))
* **SSH Node:** Cleanup temporary binary files as soon as possible ([#11305](https://github.com/n8n-io/n8n/issues/11305)) ([08a7b5b](https://github.com/n8n-io/n8n/commit/08a7b5b7425663ec6593114921c2e22ab37d039e))
### Features
* Add report bug buttons ([#11304](https://github.com/n8n-io/n8n/issues/11304)) ([296f68f](https://github.com/n8n-io/n8n/commit/296f68f041b93fd32ac7be2b53c2b41d58c2998a))
* **AI Agent Node:** Make tools optional when using OpenAI model with Tools agent ([#11212](https://github.com/n8n-io/n8n/issues/11212)) ([fed7c3e](https://github.com/n8n-io/n8n/commit/fed7c3ec1fb0553adaa9a933f91aabfd54fe83a3))
* **core:** introduce JWT API keys for the public API ([#11005](https://github.com/n8n-io/n8n/issues/11005)) ([679fa4a](https://github.com/n8n-io/n8n/commit/679fa4a10a85fc96e12ca66fe12cdb32368bc12b))
* **core:** Enforce config file permissions on startup ([#11328](https://github.com/n8n-io/n8n/issues/11328)) ([c078a51](https://github.com/n8n-io/n8n/commit/c078a516bec857831cc904ef807d0791b889f3a2))
* **core:** Handle cycles in workflows when partially executing them ([#11187](https://github.com/n8n-io/n8n/issues/11187)) ([321d6de](https://github.com/n8n-io/n8n/commit/321d6deef18806d88d97afef2f2c6f29e739ccb4))
* **editor:** Separate node output execution tooltip from status icon ([#11196](https://github.com/n8n-io/n8n/issues/11196)) ([cd15e95](https://github.com/n8n-io/n8n/commit/cd15e959c7af82a7d8c682e94add2b2640624a70))
* **GitHub Node:** Add workflow resource operations ([#10744](https://github.com/n8n-io/n8n/issues/10744)) ([d309112](https://github.com/n8n-io/n8n/commit/d3091126472faa2c8f270650e54027d19dc56bb6))
* **n8n Form Page Node:** New node ([#10390](https://github.com/n8n-io/n8n/issues/10390)) ([643d66c](https://github.com/n8n-io/n8n/commit/643d66c0ae084a0d93dac652703adc0a32cab8de))
* **n8n Google My Business Node:** New node ([#10504](https://github.com/n8n-io/n8n/issues/10504)) ([bf28fbe](https://github.com/n8n-io/n8n/commit/bf28fbefe5e8ba648cba1555a2d396b75ee32bbb))
* Run `mfa.beforeSetup` hook before enabling MFA ([#11116](https://github.com/n8n-io/n8n/issues/11116)) ([25c1c32](https://github.com/n8n-io/n8n/commit/25c1c3218cf1075ca3abd961236f3b2fbd9d6ba9))
* **Structured Output Parser Node:** Refactor Output Parsers and Improve Error Handling ([#11148](https://github.com/n8n-io/n8n/issues/11148)) ([45274f2](https://github.com/n8n-io/n8n/commit/45274f2e7f081e194e330e1c9e6a5c26fca0b141))
# [1.64.0](https://github.com/n8n-io/n8n/compare/n8n@1.63.0...n8n@1.64.0) (2024-10-16)
### Bug Fixes
* Adjust arrow button colors in dark mode ([#11248](https://github.com/n8n-io/n8n/issues/11248)) ([439132c](https://github.com/n8n-io/n8n/commit/439132c291a812d57702c94eaa12878394ac4c69))
* **core:** Ensure error reporter does not promote `info` to `error` messages ([#11245](https://github.com/n8n-io/n8n/issues/11245)) ([a7fc7fc](https://github.com/n8n-io/n8n/commit/a7fc7fc22997acec86dc94386c95349fd018f4ae))
* **core:** Override executions mode if `regular` during worker startup ([#11250](https://github.com/n8n-io/n8n/issues/11250)) ([c0aa28c](https://github.com/n8n-io/n8n/commit/c0aa28c6cf3f77b04e04663217c9df8e3803ed3f))
* **core:** Wrap nodes for tool use at a suitable time ([#11238](https://github.com/n8n-io/n8n/issues/11238)) ([c2fb881](https://github.com/n8n-io/n8n/commit/c2fb881d61291209802438d95892d052f5c82d43))
* Don't show pinned data tooltip for pinned nodes ([#11249](https://github.com/n8n-io/n8n/issues/11249)) ([c2ad156](https://github.com/n8n-io/n8n/commit/c2ad15646d326a8f71e314d54efe202a5bcdd296))
* **editor:** Bring back the "Forgot password" link on SigninView ([#11216](https://github.com/n8n-io/n8n/issues/11216)) ([4e78c46](https://github.com/n8n-io/n8n/commit/4e78c46a7450c7fc0694369944d4fb446cef2348))
* **editor:** Fix chat crashing when rendering output-parsed content ([#11210](https://github.com/n8n-io/n8n/issues/11210)) ([4aaebfd](https://github.com/n8n-io/n8n/commit/4aaebfd4358f590e98c453ad4e65cc2c9d0f76f8))
* **editor:** Make submit in ChangePasswordView work again ([#11227](https://github.com/n8n-io/n8n/issues/11227)) ([4f27b39](https://github.com/n8n-io/n8n/commit/4f27b39b45b58779d363980241e6e5e11b58f5da))
* Expressions display actual result of evaluating expression inside string ([#11257](https://github.com/n8n-io/n8n/issues/11257)) ([7f5f0a9](https://github.com/n8n-io/n8n/commit/7f5f0a9df3b3fae6e2f9787443ac1cf9415d5932))
* **Google Ads Node:** Update to use v17 api ([#11243](https://github.com/n8n-io/n8n/issues/11243)) ([3d97f02](https://github.com/n8n-io/n8n/commit/3d97f02a8d2b6e5bc7c97c5271bed97417ecacd2))
* **Google Calendar Node:** Fix issue with conference data types not loading ([#11185](https://github.com/n8n-io/n8n/issues/11185)) ([4012758](https://github.com/n8n-io/n8n/commit/401275884e5db0287e4eeffb3c7497dd5e024880))
* **Google Calendar Node:** Mode to add or replace attendees in event update ([#11132](https://github.com/n8n-io/n8n/issues/11132)) ([6c6a8ef](https://github.com/n8n-io/n8n/commit/6c6a8efdea83cf7194304ce089d7b72d8f6c1a9d))
* **HTTP Request Tool Node:** Respond with an error when receive binary response ([#11219](https://github.com/n8n-io/n8n/issues/11219)) ([0d23a7f](https://github.com/n8n-io/n8n/commit/0d23a7fb5ba41545f70c4848d30b90af91b1e7e6))
* **MySQL Node:** Fix "Maximum call stack size exceeded" error when handling a large number of rows ([#11242](https://github.com/n8n-io/n8n/issues/11242)) ([b7ee0c4](https://github.com/n8n-io/n8n/commit/b7ee0c4087eae346bc7e5360130d6c812dbe99db))
* **n8n Trigger Node:** Merge with Workflow Trigger node ([#11174](https://github.com/n8n-io/n8n/issues/11174)) ([6ec6b51](https://github.com/n8n-io/n8n/commit/6ec6b5197ae97eb86496effd458fcc0b9b223ef3))
* **OpenAI Node:** Fix tool parameter parsing issue ([#11201](https://github.com/n8n-io/n8n/issues/11201)) ([5a1d81a](https://github.com/n8n-io/n8n/commit/5a1d81ad917fde5cd6a387fe2d4ec6aab6b71349))
* **Set Node:** Fix issue with UI properties not being hidden ([#11263](https://github.com/n8n-io/n8n/issues/11263)) ([1affc27](https://github.com/n8n-io/n8n/commit/1affc27b6bf9a559061a06f92bebe8167d938665))
* **Strava Trigger Node:** Fix issue with webhook not being deleted ([#11226](https://github.com/n8n-io/n8n/issues/11226)) ([566529c](https://github.com/n8n-io/n8n/commit/566529ca1149988a54a58b3c34bbe4d9f1add6db))
### Features
* Add tracking for node errors and update node graph ([#11060](https://github.com/n8n-io/n8n/issues/11060)) ([d3b05f1](https://github.com/n8n-io/n8n/commit/d3b05f1c54e62440666297d8e484ccd22168da48))
* **core:** Dedupe ([#10101](https://github.com/n8n-io/n8n/issues/10101)) ([52dd2c7](https://github.com/n8n-io/n8n/commit/52dd2c76196c6895b47145c2b85a6895ce2874d4))
* **editor:** Send workflow context to assistant store ([#11135](https://github.com/n8n-io/n8n/issues/11135)) ([fade9e4](https://github.com/n8n-io/n8n/commit/fade9e43c84a0ae1fbc80f3ee546a418970e2380))
* **Gong Node:** New node ([#10777](https://github.com/n8n-io/n8n/issues/10777)) ([785b47f](https://github.com/n8n-io/n8n/commit/785b47feb3b83cf36aaed57123f8baca2bbab307))
### Performance Improvements
* **Google Sheets Node:** Don't load whole spreadsheet dataset to determine columns when appending data ([#11235](https://github.com/n8n-io/n8n/issues/11235)) ([26ad091](https://github.com/n8n-io/n8n/commit/26ad091f473bca4e5d3bdc257e0818be02e52db5))
# [1.63.0](https://github.com/n8n-io/n8n/compare/n8n@1.62.1...n8n@1.63.0) (2024-10-09)
### Bug Fixes
* **Convert to File Node:** Convert to ICS start date defaults to now ([#11114](https://github.com/n8n-io/n8n/issues/11114)) ([1146c4e](https://github.com/n8n-io/n8n/commit/1146c4e98d8c85c15ac67fa1c3bfb731234531e3))
* **core:** Allow loading nodes from multiple custom directories ([#11130](https://github.com/n8n-io/n8n/issues/11130)) ([1b84b0e](https://github.com/n8n-io/n8n/commit/1b84b0e5e7485d9f99d61a8ae3df49efadca0745))
* **core:** Always set `startedAt` when executions start running ([#11098](https://github.com/n8n-io/n8n/issues/11098)) ([722f4a8](https://github.com/n8n-io/n8n/commit/722f4a8b771058800b992a482ad5f644b650960d))
* **core:** Fix AI nodes not working with new partial execution flow ([#11055](https://github.com/n8n-io/n8n/issues/11055)) ([0eee5df](https://github.com/n8n-io/n8n/commit/0eee5dfd597817819dbe0463a63f671fde53432f))
* **core:** Print errors that happen before the execution starts on the worker instead of just on the main instance ([#11099](https://github.com/n8n-io/n8n/issues/11099)) ([1d14557](https://github.com/n8n-io/n8n/commit/1d145574611661ecd9ab1a39d815c0ea915b9a1c))
* **core:** Separate error handlers for main and worker ([#11091](https://github.com/n8n-io/n8n/issues/11091)) ([bb59cc7](https://github.com/n8n-io/n8n/commit/bb59cc71acc9e494e54abc8402d58db39e5a664e))
* **editor:** Shorten overflowing Node Label in InputLabels on hover and focus ([#11110](https://github.com/n8n-io/n8n/issues/11110)) ([87a0b68](https://github.com/n8n-io/n8n/commit/87a0b68f9009c1c776d937c6ca62096e88c95ed6))
* **editor:** Add safety to prevent undefined errors ([#11104](https://github.com/n8n-io/n8n/issues/11104)) ([565b117](https://github.com/n8n-io/n8n/commit/565b117a52f8eac9202a1a62c43daf78b293dcf8))
* **editor:** Fix design system form element sizing ([#11040](https://github.com/n8n-io/n8n/issues/11040)) ([67c3453](https://github.com/n8n-io/n8n/commit/67c3453885bc619fedc8338a6dd0d8d66dead931))
* **editor:** Fix getInitials when Intl.Segmenter is not supported ([#11103](https://github.com/n8n-io/n8n/issues/11103)) ([7e8955b](https://github.com/n8n-io/n8n/commit/7e8955b322b1d2c84c0f479a5977484d8d5e3135))
* **editor:** Fix schema view in AI tools ([#11089](https://github.com/n8n-io/n8n/issues/11089)) ([09cfdbd](https://github.com/n8n-io/n8n/commit/09cfdbd1817eba46c935308880fe9f95ded252b0))
* **editor:** Respect tag querystring filter when listing workflows ([#11029](https://github.com/n8n-io/n8n/issues/11029)) ([59c5ff6](https://github.com/n8n-io/n8n/commit/59c5ff61354302562ba5a2340c66811afdd1523b))
* **editor:** Show previous nodes autocomplete in AI tool nodes ([#11111](https://github.com/n8n-io/n8n/issues/11111)) ([8566b3a](https://github.com/n8n-io/n8n/commit/8566b3a99939f45ac263830eee30d0d4ade9305c))
* **editor:** Update Usage page for Community+ edition ([#11074](https://github.com/n8n-io/n8n/issues/11074)) ([3974981](https://github.com/n8n-io/n8n/commit/3974981ea5c67f6f2bbb90a96b405d9d0cfa21af))
* Fix transaction handling for 'revert' command ([#11145](https://github.com/n8n-io/n8n/issues/11145)) ([a782336](https://github.com/n8n-io/n8n/commit/a7823367f13c3dba0c339eaafaad0199bd524b13))
* Forbid access to files outside source control work directory ([#11152](https://github.com/n8n-io/n8n/issues/11152)) ([606eedb](https://github.com/n8n-io/n8n/commit/606eedbf1b302e153bd13b7cef80847711e3a9ee))
* **Gitlab Node:** Author name and email not being set ([#11077](https://github.com/n8n-io/n8n/issues/11077)) ([fce1233](https://github.com/n8n-io/n8n/commit/fce1233b58624d502c9c68f4b32a4bb7d76f1814))
* Incorrect error message on calling wrong webhook method ([#11093](https://github.com/n8n-io/n8n/issues/11093)) ([d974b01](https://github.com/n8n-io/n8n/commit/d974b015d030c608158ff0c3fa3b7f4cbb8eadd3))
* **n8n Form Trigger Node:** When clicking on a multiple choice label, the wrong one is selected ([#11059](https://github.com/n8n-io/n8n/issues/11059)) ([948edd1](https://github.com/n8n-io/n8n/commit/948edd1a047cf3dbddb3b0e9ec5de4bac3e97b9f))
* **NASA Node:** Astronomy-Picture-Of-The-Day fails when it's YouTube video ([#11046](https://github.com/n8n-io/n8n/issues/11046)) ([c70969d](https://github.com/n8n-io/n8n/commit/c70969da2bcabeb33394073a69ccef208311461b))
* **Postgres PGVector Store Node:** Fix filtering in retriever mode ([#11075](https://github.com/n8n-io/n8n/issues/11075)) ([dbd2ae1](https://github.com/n8n-io/n8n/commit/dbd2ae199506a24c2df4c983111a56f2adf63eee))
* Show result of waiting execution on canvas after execution complete ([#10815](https://github.com/n8n-io/n8n/issues/10815)) ([90b4bfc](https://github.com/n8n-io/n8n/commit/90b4bfc472ef132d2280b175ae7410dfb8e549b2))
* **Slack Node:** User id not sent correctly to API when updating user profile ([#11153](https://github.com/n8n-io/n8n/issues/11153)) ([ed9e61c](https://github.com/n8n-io/n8n/commit/ed9e61c46055d8e636a70c9c175d7d4ba596dd48))
### Features
* **core:** Introduce scoped logging ([#11127](https://github.com/n8n-io/n8n/issues/11127)) ([c68782c](https://github.com/n8n-io/n8n/commit/c68782c633b7ef6253ea705c5a222d4536491fd5))
* **editor:** Add navigation dropdown component ([#11047](https://github.com/n8n-io/n8n/issues/11047)) ([e081fd1](https://github.com/n8n-io/n8n/commit/e081fd1f0b5a0700017a8dc92f013f0abdbad319))
* **editor:** Add route for create / edit / share credentials ([#11134](https://github.com/n8n-io/n8n/issues/11134)) ([5697de4](https://github.com/n8n-io/n8n/commit/5697de4429c5d94f25ce1bd14c84fb4266ea47a7))
* **editor:** Community+ enrollment ([#10776](https://github.com/n8n-io/n8n/issues/10776)) ([92cf860](https://github.com/n8n-io/n8n/commit/92cf860f9f2994442facfddc758bc60f5cbec520))
* Human in the loop ([#10675](https://github.com/n8n-io/n8n/issues/10675)) ([41228b4](https://github.com/n8n-io/n8n/commit/41228b472de11affc8cd0821284427c2c9e8b421))
* **OpenAI Node:** Allow to specify thread ID for Assistant -> Message operation ([#11080](https://github.com/n8n-io/n8n/issues/11080)) ([6a2f9e7](https://github.com/n8n-io/n8n/commit/6a2f9e72959fb0e89006b69c31fbcee1ead1cde9))
* Opt in to additional features on community for existing users ([#11166](https://github.com/n8n-io/n8n/issues/11166)) ([c2adfc8](https://github.com/n8n-io/n8n/commit/c2adfc85451c5103eaad068f882066fd36c4aebe))
### Performance Improvements
* **core:** Optimize worker healthchecks ([#11092](https://github.com/n8n-io/n8n/issues/11092)) ([19fb728](https://github.com/n8n-io/n8n/commit/19fb728da0839c57603e55da4e407715e6c5b081))
## [1.62.1](https://github.com/n8n-io/n8n/compare/n8n@1.61.0...n8n@1.62.1) (2024-10-02)
### Bug Fixes
* **AI Agent Node:** Fix output parsing and empty tool input handling in AI Agent node ([#10970](https://github.com/n8n-io/n8n/issues/10970)) ([3a65bdc](https://github.com/n8n-io/n8n/commit/3a65bdc1f522932d463b4da0e67d29076887d06c))
* **API:** Fix workflow project transfer ([#10651](https://github.com/n8n-io/n8n/issues/10651)) ([5f89e3a](https://github.com/n8n-io/n8n/commit/5f89e3a01c1bbb3589ff0464fd5bc991426f55dc))
* **AwsS3 Node:** Fix search only using first input parameters ([#10998](https://github.com/n8n-io/n8n/issues/10998)) ([846cfde](https://github.com/n8n-io/n8n/commit/846cfde8dcaf7bf80f0a4ca7d65fc2a7b61d0e23))
* **Chat Trigger Node:** Fix Allowed Origins paramter ([#11011](https://github.com/n8n-io/n8n/issues/11011)) ([b5f4afe](https://github.com/n8n-io/n8n/commit/b5f4afe12ec77f527080a4b7f812e12f9f73f8df))
* **core:** Fix ownerless project case in statistics service ([#11051](https://github.com/n8n-io/n8n/issues/11051)) ([bdaadf1](https://github.com/n8n-io/n8n/commit/bdaadf10e058e2c0b1141289189d6526c030a2ca))
* **core:** Handle Redis disconnects gracefully ([#11007](https://github.com/n8n-io/n8n/issues/11007)) ([cd91648](https://github.com/n8n-io/n8n/commit/cd916480c2d2b55f2215c72309dc432340fc3f30))
* **core:** Prevent backend from loading duplicate copies of nodes packages ([#10979](https://github.com/n8n-io/n8n/issues/10979)) ([4584f22](https://github.com/n8n-io/n8n/commit/4584f22a9b16883779d8555cda309fd8bd113f6c))
* **core:** Upgrade @n8n/typeorm to address a rare mutex release issue ([#10993](https://github.com/n8n-io/n8n/issues/10993)) ([2af0fbf](https://github.com/n8n-io/n8n/commit/2af0fbf52f0b404697f5148f81ad0035c9ffb6b9))
* **editor:** Allow resources to move between personal and team projects ([#10683](https://github.com/n8n-io/n8n/issues/10683)) ([136d491](https://github.com/n8n-io/n8n/commit/136d49132567558b7d27069c857c0e0bfee70ce2))
* **editor:** Color scheme for a markdown code blocks in dark mode ([#11008](https://github.com/n8n-io/n8n/issues/11008)) ([b20d2eb](https://github.com/n8n-io/n8n/commit/b20d2eb403f71fe1dc21c92df118adcebef51ffe))
* **editor:** Fix filter execution by "Queued" ([#10987](https://github.com/n8n-io/n8n/issues/10987)) ([819d20f](https://github.com/n8n-io/n8n/commit/819d20fa2eee314b88a7ce1c4db632afac514704))
* **editor:** Fix performance issue in credentials list ([#10988](https://github.com/n8n-io/n8n/issues/10988)) ([7073ec6](https://github.com/n8n-io/n8n/commit/7073ec6fe5384cc8c50dcb242212999a1fbc9041))
* **editor:** Fix schema view pill highlighting ([#10936](https://github.com/n8n-io/n8n/issues/10936)) ([1b973dc](https://github.com/n8n-io/n8n/commit/1b973dcd8dbce598e6ada490fd48fad52f7b4f3a))
* **editor:** Fix workflow executions list page redirection ([#10981](https://github.com/n8n-io/n8n/issues/10981)) ([fe7d060](https://github.com/n8n-io/n8n/commit/fe7d0605681dc963f5e5d1607f9d40c5173e0f9f))
* **editor:** Format action names properly when action is not defined ([#11030](https://github.com/n8n-io/n8n/issues/11030)) ([9c43fb3](https://github.com/n8n-io/n8n/commit/9c43fb301d1ccb82e42f46833e19587289803cd3))
* **Elasticsearch Node:** Fix issue with self signed certificates not working ([#10954](https://github.com/n8n-io/n8n/issues/10954)) ([79622b5](https://github.com/n8n-io/n8n/commit/79622b5f267f2a4a53f3eb48e228939d6e3a9caa))
* **Facebook Lead Ads Trigger Node:** Pagination fix in RLC ([#10956](https://github.com/n8n-io/n8n/issues/10956)) ([6322372](https://github.com/n8n-io/n8n/commit/632237261087ada0177b67922f9f48ca02ef1d9e))
* **Github Document Loader Node:** Pass through apiUrl from credentials & fix log output ([#11049](https://github.com/n8n-io/n8n/issues/11049)) ([a7af981](https://github.com/n8n-io/n8n/commit/a7af98183c47a5e215869c8269729b0fb2f318b5))
* **Google Sheets Node:** Updating on row_number using automatic matching ([#10940](https://github.com/n8n-io/n8n/issues/10940)) ([ed91495](https://github.com/n8n-io/n8n/commit/ed91495ebc1e09b89533ffef4b775eaa0139f365))
* **HTTP Request Tool Node:** Remove default user agent header ([#10971](https://github.com/n8n-io/n8n/issues/10971)) ([5a99e93](https://github.com/n8n-io/n8n/commit/5a99e93f8d2c66d7dbcef382478badd63bc4a0b5))
* **Postgres Node:** Falsy query parameters ignored ([#10960](https://github.com/n8n-io/n8n/issues/10960)) ([4a63cff](https://github.com/n8n-io/n8n/commit/4a63cff5ec722c810e3ff2bd7b0bb1e32f7f403b))
* **Respond to Webhook Node:** Node does not work with Wait node ([#10992](https://github.com/n8n-io/n8n/issues/10992)) ([2df5a5b](https://github.com/n8n-io/n8n/commit/2df5a5b649f8ba3b747782d6d5045820aa74955d))
* **RSS Feed Trigger Node:** Fix regression on missing timestamps ([#10991](https://github.com/n8n-io/n8n/issues/10991)) ([d2bc076](https://github.com/n8n-io/n8n/commit/d2bc0760e2b5c977fcc683f0a0281f099a9c538d))
* **Supabase Node:** Fix issue with delete not always working ([#10952](https://github.com/n8n-io/n8n/issues/10952)) ([1944b46](https://github.com/n8n-io/n8n/commit/1944b46fd472bb59552b5fbf7783168a622a2bd2))
* **Text Classifier Node:** Default system prompt template ([#11018](https://github.com/n8n-io/n8n/issues/11018)) ([77fec19](https://github.com/n8n-io/n8n/commit/77fec195d92e0fe23c60552a72e8c030cf7e5e5c))
* **Todoist Node:** Fix listSearch filter bug in Todoist Node ([#10989](https://github.com/n8n-io/n8n/issues/10989)) ([c4b3272](https://github.com/n8n-io/n8n/commit/c4b327248d7aa1352e8d6acec5627ff406aea3d4))
* **Todoist Node:** Make Section Name optional in Move Task operation ([#10732](https://github.com/n8n-io/n8n/issues/10732)) ([799006a](https://github.com/n8n-io/n8n/commit/799006a3cce6abe210469c839ae392d0c1aec486))
### Features
* Add more context to support chat ([#11014](https://github.com/n8n-io/n8n/issues/11014)) ([8a30f92](https://github.com/n8n-io/n8n/commit/8a30f92156d6a4fe73113bd3cdfb751b8c9ce4b4))
* Add Sysdig API credentials for SecOps ([#7033](https://github.com/n8n-io/n8n/issues/7033)) ([a8d1a1e](https://github.com/n8n-io/n8n/commit/a8d1a1ea854fb2c69643b0a5738440b389121ca3))
* **core:** Filter executions by project ID in internal API ([#10976](https://github.com/n8n-io/n8n/issues/10976)) ([06d749f](https://github.com/n8n-io/n8n/commit/06d749ffa7ced503141d8b07e22c47d971eb1623))
* **core:** Implement Dynamic Parameters within regular nodes used as AI Tools ([#10862](https://github.com/n8n-io/n8n/issues/10862)) ([ef5b7cf](https://github.com/n8n-io/n8n/commit/ef5b7cf9b77b653111eb5b1d9de8116c9f6b9f92))
* **editor:** Do not show error for remote options when credentials aren't specified ([#10944](https://github.com/n8n-io/n8n/issues/10944)) ([9fc3699](https://github.com/n8n-io/n8n/commit/9fc3699beb0c150909889ed17740a5cd9e0461c3))
* **editor:** Enable drag and drop in code editors (Code/SQL/HTML) ([#10888](https://github.com/n8n-io/n8n/issues/10888)) ([af9e227](https://github.com/n8n-io/n8n/commit/af9e227ad4848995b9d82c72f814dbf9d1de506f))
* **editor:** Overhaul document title management ([#10999](https://github.com/n8n-io/n8n/issues/10999)) ([bb28956](https://github.com/n8n-io/n8n/commit/bb2895689fb006897bc244271aca6f0bfa1839b9))
* **editor:** Remove execution annotation feature flag ([#11020](https://github.com/n8n-io/n8n/issues/11020)) ([e7199db](https://github.com/n8n-io/n8n/commit/e7199dbfccdbdf1c4273f916e3006ca610c230e9))
* **editor:** Support node-creator actions for vector store nodes ([#11032](https://github.com/n8n-io/n8n/issues/11032)) ([72b70d9](https://github.com/n8n-io/n8n/commit/72b70d9d98daeba654baf6785ff1ae234c73c977))
* **Google BigQuery Node:** Return numeric values as integers ([#10943](https://github.com/n8n-io/n8n/issues/10943)) ([d7c1d24](https://github.com/n8n-io/n8n/commit/d7c1d24f74648740b2f425640909037ba06c5030))
* **Invoice Ninja Node:** Add more query params to getAll requests ([#9238](https://github.com/n8n-io/n8n/issues/9238)) ([50b7238](https://github.com/n8n-io/n8n/commit/50b723836e70bbe405594f690b73057f9c33fbe4))
* **Iterable Node:** Add support for EDC and USDC selection ([#10908](https://github.com/n8n-io/n8n/issues/10908)) ([0ca9c07](https://github.com/n8n-io/n8n/commit/0ca9c076ca51d313392e45c3b013f2e83aaea843))
* **Question and Answer Chain Node:** Customize question and answer system prompt ([#10385](https://github.com/n8n-io/n8n/issues/10385)) ([08a27b3](https://github.com/n8n-io/n8n/commit/08a27b3148aac2282f64339ddc33ac7c90835d84))
# [1.61.0](https://github.com/n8n-io/n8n/compare/n8n@1.60.0...n8n@1.61.0) (2024-09-25)
### Bug Fixes
* **core:** Add executionData to expressions in pagination code ([#10926](https://github.com/n8n-io/n8n/issues/10926)) ([eac103e](https://github.com/n8n-io/n8n/commit/eac103e367d59a532b9ba12db78a0dd10aee62fb))
* **core:** Fix webhook binary data max size configuration ([#10897](https://github.com/n8n-io/n8n/issues/10897)) ([693fb7e](https://github.com/n8n-io/n8n/commit/693fb7e580b7e030c86977bff6d319bbee4fcd62))
* **core:** Remove subworkflow license check ([#10893](https://github.com/n8n-io/n8n/issues/10893)) ([0290e38](https://github.com/n8n-io/n8n/commit/0290e38f990275074eb7e7ccd0b41f1ae0215dd2))
* **editor:** Credentials scopes and n8n scopes mix up ([#10930](https://github.com/n8n-io/n8n/issues/10930)) ([e069608](https://github.com/n8n-io/n8n/commit/e0696080227aee7ccb50d51a82873e8a1ba4667d))
* **editor:** Fix design system form component sizing ([#10961](https://github.com/n8n-io/n8n/issues/10961)) ([cf153ea](https://github.com/n8n-io/n8n/commit/cf153ea085165115ee523fbb1bd32080dde47eda))
* **editor:** Fix modal overflow when AI is enabled in code node ([#10887](https://github.com/n8n-io/n8n/issues/10887)) ([f9f303f](https://github.com/n8n-io/n8n/commit/f9f303f562084db8c8956da267680b1f935aa2df))
* **editor:** Fix source control push modal checkboxes ([#10910](https://github.com/n8n-io/n8n/issues/10910)) ([8db8817](https://github.com/n8n-io/n8n/commit/8db88178511749b19a5878816ef062092fd9f2be))
* **editor:** Fix styling and typography in AI Assistant chat ([#10895](https://github.com/n8n-io/n8n/issues/10895)) ([57ff3cc](https://github.com/n8n-io/n8n/commit/57ff3cc27b9470bfbe2486c3c1831c57f5a4075f))
* **editor:** Prevent clipboard xss injection ([#10894](https://github.com/n8n-io/n8n/issues/10894)) ([e20ab59](https://github.com/n8n-io/n8n/commit/e20ab59c1dcf9da19a30268ce19930bfa7e38992))
* **editor:** Prevent node name input in NDV to expand unnecessarily ([#10922](https://github.com/n8n-io/n8n/issues/10922)) ([a2237d1](https://github.com/n8n-io/n8n/commit/a2237d128ff6a4d65cd30325b6b9d9b765ca7be6))
* **editor:** Update gird size when opening credentials support chat ([#10882](https://github.com/n8n-io/n8n/issues/10882)) ([b86fd80](https://github.com/n8n-io/n8n/commit/b86fd80fc9fe06011367ca04a75e4b52533db1fe))
* **editor:** Use `:focus-visible` instead for `:focus` for buttons ([#10921](https://github.com/n8n-io/n8n/issues/10921)) ([bf28d09](https://github.com/n8n-io/n8n/commit/bf28d0965c46620a106c87037bafd2cf936f1050))
* **editor:** Use correct output for connected nodes in schema view ([#10928](https://github.com/n8n-io/n8n/issues/10928)) ([ad60d49](https://github.com/n8n-io/n8n/commit/ad60d49b4251138a7c69cb5e9f00c3ef875486e0))
* Enable Assistant on other credential views ([#10931](https://github.com/n8n-io/n8n/issues/10931)) ([557db9c](https://github.com/n8n-io/n8n/commit/557db9c170a89447ec9cc14aa1af51e5fd11dd92))
* Ensure user id for early track events ([#10885](https://github.com/n8n-io/n8n/issues/10885)) ([23c09ea](https://github.com/n8n-io/n8n/commit/23c09eae4223545c717270a5cd305d2e57e1ad5b))
* **Google Sheets Node:** Insert data if sheet is empty instead of error ([#10942](https://github.com/n8n-io/n8n/issues/10942)) ([c75990e](https://github.com/n8n-io/n8n/commit/c75990e0632c581384542610a886ef89621a9403))
* Hide assistant button when showing Click to connect ([#10932](https://github.com/n8n-io/n8n/issues/10932)) ([d74cff2](https://github.com/n8n-io/n8n/commit/d74cff20301f285588f93207f29660d25fdbc8da))
* **HTTP Request Node:** Do not modify request object when sanitizing message for UI ([#10923](https://github.com/n8n-io/n8n/issues/10923)) ([8cc10cc](https://github.com/n8n-io/n8n/commit/8cc10cc2c1869b9abcafd157e41be65ce2b6f499))
* **MQTT Node:** Close connection if connection attempt fails ([#10873](https://github.com/n8n-io/n8n/issues/10873)) ([ee7147c](https://github.com/n8n-io/n8n/commit/ee7147c6b3b053ac8fc317319ab257204e599f16))
* **MySQL Node:** Fix "Maximum call stack size exceeded" error when handling a large number of rows ([#10965](https://github.com/n8n-io/n8n/issues/10965)) ([62159bd](https://github.com/n8n-io/n8n/commit/62159bd71c9a0303b597a68113e0ac50473ee8d4))
* **Notion Node:** Allow UUID v8 in notion id checks ([#10938](https://github.com/n8n-io/n8n/issues/10938)) ([46beda0](https://github.com/n8n-io/n8n/commit/46beda05f6771c31bcf0b6a781976d8261079a66))
### Features
* **Brandfetch Node:** Update to use new API ([#10877](https://github.com/n8n-io/n8n/issues/10877)) ([08ba9a3](https://github.com/n8n-io/n8n/commit/08ba9a36a43b6c84f69bb04fa4d6419a7a4adddf))
* **editor:** Setup Sentry integration ([#10945](https://github.com/n8n-io/n8n/issues/10945)) ([6de4dff](https://github.com/n8n-io/n8n/commit/6de4dfff87e4da888567081a9928d9682bdea11d))
* **editor:** Show a notice before deleting annotated executions ([#10934](https://github.com/n8n-io/n8n/issues/10934)) ([dcc1c72](https://github.com/n8n-io/n8n/commit/dcc1c72fc4b56c3252183541b22da801804d4f79))
* Page size 1 option ([#10957](https://github.com/n8n-io/n8n/issues/10957)) ([bdc0622](https://github.com/n8n-io/n8n/commit/bdc0622f59e98c9e6c542f5cb59a2dbd9008ba96))
* **Slack Node:** Add option to hide workflow link on message update ([#10927](https://github.com/n8n-io/n8n/issues/10927)) ([422c946](https://github.com/n8n-io/n8n/commit/422c9463c8d931a728615a1fe5a10f05a96ecaa2))
### Performance Improvements
* **editor:** Use virtual scrolling in `RunDataJson.vue` ([#10838](https://github.com/n8n-io/n8n/issues/10838)) ([f5474ff](https://github.com/n8n-io/n8n/commit/f5474ff79198a2f5a145d0a9df1bb651ea677ec5))
# [1.60.0](https://github.com/n8n-io/n8n/compare/n8n@1.59.0...n8n@1.60.0) (2024-09-18)
### Bug Fixes
* **Azure OpenAI Chat Model Node:** Add response format option ([#10851](https://github.com/n8n-io/n8n/issues/10851)) ([0b5299a](https://github.com/n8n-io/n8n/commit/0b5299a248fdd451ceabb98ff6a2b38e818d02f8))
* **Contentful Node:** Add missing additional fields to entry > get ([#10830](https://github.com/n8n-io/n8n/issues/10830)) ([c43aef1](https://github.com/n8n-io/n8n/commit/c43aef1a266cc6ccf8f778c290f8cb8ba2ee28cf))
* **core:** Prevent shutdown error in regular mode ([#10844](https://github.com/n8n-io/n8n/issues/10844)) ([acb4194](https://github.com/n8n-io/n8n/commit/acb4194fa1a1d0497dd1f48045f069e1db28c432))
* **core:** Restore queue listeners for `webhook` process ([#10781](https://github.com/n8n-io/n8n/issues/10781)) ([86f4877](https://github.com/n8n-io/n8n/commit/86f4877bab978a1ec2f53df23b6c515507cd8f72))
* **editor:** Add missing node parameter values to AI Assistant request ([#10788](https://github.com/n8n-io/n8n/issues/10788)) ([d65ade4](https://github.com/n8n-io/n8n/commit/d65ade4e92eed3cfc47854d493fac6885a1a852b))
* **editor:** Address edge toolbar rendering glitches ([#10839](https://github.com/n8n-io/n8n/issues/10839)) ([e0c0dde](https://github.com/n8n-io/n8n/commit/e0c0ddee59e889f50dd5033d0a933bad60fb7e3a))
* **editor:** Allow custom git repo urls in source control settings ([#10849](https://github.com/n8n-io/n8n/issues/10849)) ([a63a9b5](https://github.com/n8n-io/n8n/commit/a63a9b53f06d3a00e8e76c0ab9f2571604c01922))
* **editor:** Fix completion on $input.item. in Code node ([#10800](https://github.com/n8n-io/n8n/issues/10800)) ([45dccf3](https://github.com/n8n-io/n8n/commit/45dccf3d0c8282987833962a8e3f3a77d256ea37))
* **editor:** Make expression edit modal read-only in executions view ([#10806](https://github.com/n8n-io/n8n/issues/10806)) ([394ef88](https://github.com/n8n-io/n8n/commit/394ef888433b1d48593531ab9eea93a3c3ae6040))
* **editor:** Make schema view search copy more clear ([#10807](https://github.com/n8n-io/n8n/issues/10807)) ([7f1c131](https://github.com/n8n-io/n8n/commit/7f1c131b72ad1b98b4a8c976b8a0ef5d963d5f1f))
* **editor:** Minimap Show nodes outside viewport ([#10843](https://github.com/n8n-io/n8n/issues/10843)) ([9c95db8](https://github.com/n8n-io/n8n/commit/9c95db8282c9f3cef5568aa9793ca977d4d8a347))
* **editor:** Prevent clipboard XSS injection ([#10805](https://github.com/n8n-io/n8n/issues/10805)) ([db846d3](https://github.com/n8n-io/n8n/commit/db846d3235a360b4b729312b6ffe0d75be08fd45))
* **editor:** Render image binary-data using img tags ([#10829](https://github.com/n8n-io/n8n/issues/10829)) ([7c23101](https://github.com/n8n-io/n8n/commit/7c23101ab8c12b735a17deb35637f3f12c00aeb0))
* **editor:** Replace v-html with custom directive to sanitize html ([#10804](https://github.com/n8n-io/n8n/issues/10804)) ([44e5fb9](https://github.com/n8n-io/n8n/commit/44e5fb9b06c794033204ef1744b54b3b87160082))
* **editor:** Restore V1 keybinding, Space Key to toggle panning ([#10841](https://github.com/n8n-io/n8n/issues/10841)) ([5a1db6d](https://github.com/n8n-io/n8n/commit/5a1db6db1adad43887e839181719818474bc66b0))
* Fix telemetry causing console error ([#10828](https://github.com/n8n-io/n8n/issues/10828)) ([3be31e2](https://github.com/n8n-io/n8n/commit/3be31e27edc6e71400bde23f992ba98b2365bcff))
* **Google Vertex Chat Model Node:** Clean service account private key ([#10770](https://github.com/n8n-io/n8n/issues/10770)) ([e6d84db](https://github.com/n8n-io/n8n/commit/e6d84db89930afc16f4a08fae87d8af4a059e6d7))
* **HTTP Request Tool Node:** Fix subsequent tool calls reusung the same options ([#10808](https://github.com/n8n-io/n8n/issues/10808)) ([d647ef4](https://github.com/n8n-io/n8n/commit/d647ef41acf672177ea5e8ce0e99d78c565e34b2))
* **OpenAI Node, Basic LLM Chain Node, Tool Agent Node:** Better OpenAI API rate limit errors ([#10797](https://github.com/n8n-io/n8n/issues/10797)) ([ab83c4b](https://github.com/n8n-io/n8n/commit/ab83c4b4166d5ad5f4ca46a636f83c8802fe3ec0))
* Prevent copying workflow when copying outside of canvas ([#10813](https://github.com/n8n-io/n8n/issues/10813)) ([22c1890](https://github.com/n8n-io/n8n/commit/22c1890139c89e74df67b9673a1d0c85d647eb9d))
* **RSS Feed Trigger Node:** Handle empty items gracefully ([#10855](https://github.com/n8n-io/n8n/issues/10855)) ([c55df63](https://github.com/n8n-io/n8n/commit/c55df63abc234ace6ac8e54ed094d10797671264))
### Features
* **core:** Allow customizing max file size in form-data payloads for webhooks ([#10857](https://github.com/n8n-io/n8n/issues/10857)) ([a3335e0](https://github.com/n8n-io/n8n/commit/a3335e0ecd3796c874985d3c6fbbaabc35dc3490))
* **core:** Introduce worker metrics ([#10850](https://github.com/n8n-io/n8n/issues/10850)) ([08ebe1e](https://github.com/n8n-io/n8n/commit/08ebe1e4807b3d7b4a4840887cbb30f547a5c89a))
* **editor:** Add truncate directive ([#10842](https://github.com/n8n-io/n8n/issues/10842)) ([57836cc](https://github.com/n8n-io/n8n/commit/57836cc17a57c790d2ffb2463abb16a03321eb59))
* **editor:** Show Collaboration pane only when there are multiple active users ([#10772](https://github.com/n8n-io/n8n/issues/10772)) ([a0af1d9](https://github.com/n8n-io/n8n/commit/a0af1d9a06c78d29f215dc010332ea7c8f28717d))
* **Invoice Ninja Node:** Add actions for bank transactions ([#10389](https://github.com/n8n-io/n8n/issues/10389)) ([5a2c7e0](https://github.com/n8n-io/n8n/commit/5a2c7e00a0ca1a151a7fec56da5f99b086c25b1f))
* **OpenAI Node:** Include O1 models in the models select ([#10801](https://github.com/n8n-io/n8n/issues/10801)) ([b2b1abc](https://github.com/n8n-io/n8n/commit/b2b1abc5319bdbf2bc855649ea27359b22aba009))
# [1.59.0](https://github.com/n8n-io/n8n/compare/n8n@1.58.0...n8n@1.59.0) (2024-09-11)
### Bug Fixes
* **Chat Trigger Node:** Fix auth in "Embedded Chat" mode ([#10734](https://github.com/n8n-io/n8n/issues/10734)) ([96db501](https://github.com/n8n-io/n8n/commit/96db501a615ff7ec91bb66ea49532a2c6ca2a172))
* **core:** Allow license:clear command to be used for licenses that failed renewal ([#10665](https://github.com/n8n-io/n8n/issues/10665)) ([a422c5a](https://github.com/n8n-io/n8n/commit/a422c5ac7b8f609eeab891230d9660f71bf225c5))
* **core:** Update subworkflow execution status correctly ([#10764](https://github.com/n8n-io/n8n/issues/10764)) ([4f94319](https://github.com/n8n-io/n8n/commit/4f94319cd93885ebe830fa1f0e6b757de80f7356))
* **editor:** Add arrow end to connection line ([#10704](https://github.com/n8n-io/n8n/issues/10704)) ([43713dc](https://github.com/n8n-io/n8n/commit/43713dcd89fcb98ea7e24d27127861fc4b0d7872))
* **editor:** Add sticky note readonly state in new canvas ([#10678](https://github.com/n8n-io/n8n/issues/10678)) ([c5bc8e6](https://github.com/n8n-io/n8n/commit/c5bc8e6eb9eadadf44f763e5e5aac4b35d03cc31))
* **editor:** Auto-focus expression input when switching from "fixed" mode ([#10686](https://github.com/n8n-io/n8n/issues/10686)) ([54ab2b1](https://github.com/n8n-io/n8n/commit/54ab2b14e41fe84a455c7e7d5c73d7347844d2fb))
* **editor:** Don't render pinned icon for disabled nodes ([#10712](https://github.com/n8n-io/n8n/issues/10712)) ([879b837](https://github.com/n8n-io/n8n/commit/879b8375812106b3f6909b7de27858175ba5575d))
* **editor:** Fix error rendering and indexing of LLM sub-node outputs ([#10688](https://github.com/n8n-io/n8n/issues/10688)) ([50459ba](https://github.com/n8n-io/n8n/commit/50459bacab517bacb97d2884fda69f8412c9960c))
* **editor:** Fix xss issues in toast usages ([#10733](https://github.com/n8n-io/n8n/issues/10733)) ([6df6f5f](https://github.com/n8n-io/n8n/commit/6df6f5f8df9a8fc0899524a1b69859815eeb341f))
* **editor:** Follow up fixes and improvements to viewer role ([#10684](https://github.com/n8n-io/n8n/issues/10684)) ([63548e6](https://github.com/n8n-io/n8n/commit/63548e6ead5c122732628b5feb1515f492d5e033))
* **editor:** Increase connector snap radius ([#10757](https://github.com/n8n-io/n8n/issues/10757)) ([297b668](https://github.com/n8n-io/n8n/commit/297b668f32f9ecfc82c1205ea4e915408cab482e))
* **editor:** Plus node button should not be visible on readonly mode ([#10692](https://github.com/n8n-io/n8n/issues/10692)) ([62cb189](https://github.com/n8n-io/n8n/commit/62cb189985035c447ad31c275337b3fb24089265))
* **editor:** Prevent action's panel flickering while dragging a node ([#10739](https://github.com/n8n-io/n8n/issues/10739)) ([efa5573](https://github.com/n8n-io/n8n/commit/efa5573278a60d55d5b509aac48cc112c79334d2))
* **editor:** Restrict when the collision avoidance algorithm is used ([#10755](https://github.com/n8n-io/n8n/issues/10755)) ([bf43d67](https://github.com/n8n-io/n8n/commit/bf43d673571b2fc18fe5d660171f0da165909dfc))
* **editor:** Show docs link in credential modal when docs sidebar is hidden ([#10750](https://github.com/n8n-io/n8n/issues/10750)) ([87333cb](https://github.com/n8n-io/n8n/commit/87333cbefebe652256fa1d60ba7a4b946fdfe17d))
* **Email Trigger (IMAP) Node:** Ensure connection close does not block deactivation ([#10689](https://github.com/n8n-io/n8n/issues/10689)) ([156eb72](https://github.com/n8n-io/n8n/commit/156eb72ebefa1d963ff46eff6652e2c947ef031b))
* Fix the issue in Trigger Nodes where poll time was not loaded ([#10695](https://github.com/n8n-io/n8n/issues/10695)) ([1dea8f4](https://github.com/n8n-io/n8n/commit/1dea8f4c7da2a04434c274faf8e0a9a7a693f5a4))
* **Gmail Trigger Node:** Change Gmail Trigger dedupe logic ([#10717](https://github.com/n8n-io/n8n/issues/10717)) ([9f3e03d](https://github.com/n8n-io/n8n/commit/9f3e03d728d8acda5ae4166c5837b00cb1311e96))
* Google Contacts node warm up request, Google Calendar node events>getAll fields option ([#10700](https://github.com/n8n-io/n8n/issues/10700)) ([22c70d5](https://github.com/n8n-io/n8n/commit/22c70d50697023cf448a379d7778695abb718ce9))
* **If Node:** Update copy for type conversion parameter ([#10769](https://github.com/n8n-io/n8n/issues/10769)) ([ee5fbc5](https://github.com/n8n-io/n8n/commit/ee5fbc543ce1d33a56cf118dbd048d6693a15875))
* **n8n Form Trigger Node:** Do not rerun trigger when it has run data ([#10687](https://github.com/n8n-io/n8n/issues/10687)) ([3adbcab](https://github.com/n8n-io/n8n/commit/3adbcab27de34ea5a2c7a88b2ad0d80e3f6d4a0b))
* **OpenAI Chat Model Node:** Prevent filtering of fine-tuned models in model selector ([#10662](https://github.com/n8n-io/n8n/issues/10662)) ([4e89912](https://github.com/n8n-io/n8n/commit/4e899125884bdd97c97446d90e89668688fe7573))
* Prevent AI assistant session reset when workflow is saved ([#10707](https://github.com/n8n-io/n8n/issues/10707)) ([91d9be2](https://github.com/n8n-io/n8n/commit/91d9be20667c20599f64a24fa99386c78476d425))
* Show a more user friendly error message if initial Db connection times out ([#10682](https://github.com/n8n-io/n8n/issues/10682)) ([4efcbc5](https://github.com/n8n-io/n8n/commit/4efcbc593685286837022e5600d81e67f3e0131c))
* **Webflow Node:** Update scopes to include forms ([#10554](https://github.com/n8n-io/n8n/issues/10554)) ([d3861b3](https://github.com/n8n-io/n8n/commit/d3861b31ceef16f566c525c7651453a1b84ed2a4))
* **YouTube Node:** Fix Date filters ([#10725](https://github.com/n8n-io/n8n/issues/10725)) ([21936c8](https://github.com/n8n-io/n8n/commit/21936c88a84b8c03a8d02391cb7112b0e4d9f1f9))
### Features
* **Code Tool Node:** Option to specify input schema ([#10693](https://github.com/n8n-io/n8n/issues/10693)) ([421aa71](https://github.com/n8n-io/n8n/commit/421aa712515d9beeae7c0201b173cb7324473f69))
* **editor:** Add lint for $('Node').item in runOnceForAllItems mode ([#10743](https://github.com/n8n-io/n8n/issues/10743)) ([1b04be1](https://github.com/n8n-io/n8n/commit/1b04be1240ec29151e79162680907710c71c6488))
* **editor:** Logs markdown block improvements ([#10681](https://github.com/n8n-io/n8n/issues/10681)) ([db6e832](https://github.com/n8n-io/n8n/commit/db6e8326c7119d90fa6a51f82099026f50587202))
* Filter parameter: Improve loose type validation for booleans ([#10702](https://github.com/n8n-io/n8n/issues/10702)) ([e9b8d99](https://github.com/n8n-io/n8n/commit/e9b8d99084f0ea2063a1d691928025e534980b4e))
* **Lemlist Node:** Add V2 to support more API operations ([#10615](https://github.com/n8n-io/n8n/issues/10615)) ([20b1cf2](https://github.com/n8n-io/n8n/commit/20b1cf2b7597c78e28f522945b8cbad2ee535cd7))
* **OpenAI Node:** Add Max Tools Iteration parameter and prevent tool calling after execution is aborted ([#10735](https://github.com/n8n-io/n8n/issues/10735)) ([5c47a5f](https://github.com/n8n-io/n8n/commit/5c47a5f691d42dae84a9df8a32a5ea600d83f6dd))
### Performance Improvements
* **editor:** Fix WorkflowDetails excessive re-rendering ([#10767](https://github.com/n8n-io/n8n/issues/10767)) ([00013a2](https://github.com/n8n-io/n8n/commit/00013a2069fff5e5d9398c5921c90d34dc384299))
# [1.58.0](https://github.com/n8n-io/n8n/compare/n8n@1.57.0...n8n@1.58.0) (2024-09-05)
### Bug Fixes
* **AI Agent Node:** Fix tools agent when using memory and Anthropic models ([#10513](https://github.com/n8n-io/n8n/issues/10513)) ([746e7b8](https://github.com/n8n-io/n8n/commit/746e7b89f7e9b99126fb69110773548dfe91b74f))
* **API:** Update express-openapi-validator to resolve AIKIDO-2024-10229 ([#10612](https://github.com/n8n-io/n8n/issues/10612)) ([1dcb814](https://github.com/n8n-io/n8n/commit/1dcb814ced7cfbc80eddbb4bc03108341a9f27f5))
* **core:** Declutter webhook insertion errors ([#10650](https://github.com/n8n-io/n8n/issues/10650)) ([36177b0](https://github.com/n8n-io/n8n/commit/36177b0943cf72bae3b0075453498dd1e41684d0))
* **core:** Flush responses for ai streaming endpoints ([#10633](https://github.com/n8n-io/n8n/issues/10633)) ([6bb6a5c](https://github.com/n8n-io/n8n/commit/6bb6a5c6cd1da3503a1a2b35bcf4c685cd3f964f))
* **core:** Tighten check for company size survey answer ([#10646](https://github.com/n8n-io/n8n/issues/10646)) ([e5aba60](https://github.com/n8n-io/n8n/commit/e5aba60afff93364d91f17c00ea18d38d9dbc970))
* **editor:** Add confirmation toast when changing user role ([#10592](https://github.com/n8n-io/n8n/issues/10592)) ([95da4d4](https://github.com/n8n-io/n8n/commit/95da4d4797e800c04b2b17c23c941c785dd62393))
* **editor:** Add pinned data only to manual executions in execution view ([#10605](https://github.com/n8n-io/n8n/issues/10605)) ([a12e9ed](https://github.com/n8n-io/n8n/commit/a12e9edac042957939c63f0a5c35572930632352))
* **editor:** Add tooltips to workflow history button ([#10570](https://github.com/n8n-io/n8n/issues/10570)) ([4a125f5](https://github.com/n8n-io/n8n/commit/4a125f511c5537977652900b7712a2ad908140e7))
* **editor:** Allow disabling SSO when config request fails ([#10635](https://github.com/n8n-io/n8n/issues/10635)) ([ce39933](https://github.com/n8n-io/n8n/commit/ce39933766fa18107f4082de0cba0b6702cbbbfa))
* **editor:** Fix notification rendering HTML as text ([#10642](https://github.com/n8n-io/n8n/issues/10642)) ([5eba534](https://github.com/n8n-io/n8n/commit/5eba5343191665cd4639632ba303464176c279c4))
* **editor:** Fix opening executions tab from a new, unsaved workflow ([#10652](https://github.com/n8n-io/n8n/issues/10652)) ([cd0891e](https://github.com/n8n-io/n8n/commit/cd0891e4f1cfdc90b2090958a39564ba99534627))
* **Gmail Trigger Node:** Don't return date instances, but date strings instead ([#10582](https://github.com/n8n-io/n8n/issues/10582)) ([9e1dac0](https://github.com/n8n-io/n8n/commit/9e1dac04655a20c5c7b99552742312fd9237604b))
* **HTTP Request Node:** Sanitize authorization headers ([#10607](https://github.com/n8n-io/n8n/issues/10607)) ([405c55a](https://github.com/n8n-io/n8n/commit/405c55a1f7cf34e7b6e46a86031ef9a41956ca78))
* **Wait Node:** Append n8n attribution option ([#10585](https://github.com/n8n-io/n8n/issues/10585)) ([81f4322](https://github.com/n8n-io/n8n/commit/81f4322d456773281aec4b47447465bdffd311fe))
### Features
* **core:** Execution curation ([#10342](https://github.com/n8n-io/n8n/issues/10342)) ([022ddcb](https://github.com/n8n-io/n8n/commit/022ddcbef9f1ac1b89bcfd5f7759d67325b07392))
* **core:** Implement wrapping of regular nodes as AI Tools ([#10641](https://github.com/n8n-io/n8n/issues/10641)) ([da44fe4](https://github.com/n8n-io/n8n/commit/da44fe4b8967055b7b1f849750e1fafa0ba67218))
* **core:** Introduce DB health check ([#10661](https://github.com/n8n-io/n8n/issues/10661)) ([a8e80d0](https://github.com/n8n-io/n8n/commit/a8e80d0c4b7531fe32be1d4057656885359f42fc))
* **core:** Make Postgres connection timeout configurable ([#10670](https://github.com/n8n-io/n8n/issues/10670)) ([8154031](https://github.com/n8n-io/n8n/commit/81540318b4c55f3a09c9776e23d2211abdbd36f7))
* **core:** Switch to MJML for email templates ([#10518](https://github.com/n8n-io/n8n/issues/10518)) ([dbc10fe](https://github.com/n8n-io/n8n/commit/dbc10fe9f522f31eb06add6f3f6863ce24510547))
* **editor:** Add A/B testing feature flag for credential docs modal ([#10664](https://github.com/n8n-io/n8n/issues/10664)) ([899b0a1](https://github.com/n8n-io/n8n/commit/899b0a19efc49c1c087f78bbb1a59d726a510965))
* **editor:** Add AI Assistant support chat ([#10656](https://github.com/n8n-io/n8n/issues/10656)) ([3a80780](https://github.com/n8n-io/n8n/commit/3a8078068e5c0b01dfd34ff838fe1b30d604abc6))
* **editor:** Implement new app layout ([#10548](https://github.com/n8n-io/n8n/issues/10548)) ([95a9cd2](https://github.com/n8n-io/n8n/commit/95a9cd2c739cf4f817eb8df6509a9112ac24a3b1))
* **editor:** Make highlighted data pane floating ([#10638](https://github.com/n8n-io/n8n/issues/10638)) ([8b5c333](https://github.com/n8n-io/n8n/commit/8b5c333d3dca03ba51a5873b75451fbfafc5ae15))
* More hints to nodes ([#10565](https://github.com/n8n-io/n8n/issues/10565)) ([66ddb4a](https://github.com/n8n-io/n8n/commit/66ddb4a6f367602c9aaad1bfb0cc6fac3facd15e))
* **Postgres PGVector Store Node:** Add PGVector vector store node ([#10517](https://github.com/n8n-io/n8n/issues/10517)) ([650389d](https://github.com/n8n-io/n8n/commit/650389d90763a45c037e74a1a1193c3cbe103a16))
* Reintroduce collaboration feature ([#10602](https://github.com/n8n-io/n8n/issues/10602)) ([2ea2bfe](https://github.com/n8n-io/n8n/commit/2ea2bfe762c02047e522f28dd97f197735b3fb46))
* **Text Classifier Node:** Add output fixing parser ([#10667](https://github.com/n8n-io/n8n/issues/10667)) ([aa37c32](https://github.com/n8n-io/n8n/commit/aa37c32f266ffff93cd903888b1c15caa0468830))
# [1.57.0](https://github.com/n8n-io/n8n/compare/n8n@1.56.0...n8n@1.57.0) (2024-08-28)
### Bug Fixes
* **AI Agent Node:** Allow AWS Bedrock Chat to be used with conversational agent ([#10489](https://github.com/n8n-io/n8n/issues/10489)) ([bdcc657](https://github.com/n8n-io/n8n/commit/bdcc657965af5f604aac1eaff7d937f69a08ce1c))
* **core:** Make boolean config value parsing backward-compatible ([#10560](https://github.com/n8n-io/n8n/issues/10560)) ([70b410f](https://github.com/n8n-io/n8n/commit/70b410f4b00dd599fcd4249aa105098aa262da66))
* **core:** Restore Redis cache key ([#10520](https://github.com/n8n-io/n8n/issues/10520)) ([873056a](https://github.com/n8n-io/n8n/commit/873056a92e52cc629d2873c960656d5f06d4728e))
* **core:** Scheduler tasks should not trigger on follower instances ([#10507](https://github.com/n8n-io/n8n/issues/10507)) ([3428f28](https://github.com/n8n-io/n8n/commit/3428f28a732f79e067b3cb515cc59d835de246a6))
* **core:** Stop explicit redis client disconnect on shutdown ([#10551](https://github.com/n8n-io/n8n/issues/10551)) ([f712812](https://github.com/n8n-io/n8n/commit/f71281221efb79d65d8d7610c292bc90cef13d7a))
* **editor:** Ensure `Datatable` component renders `All` option ([#10525](https://github.com/n8n-io/n8n/issues/10525)) ([bc27beb](https://github.com/n8n-io/n8n/commit/bc27beb6629883003a8991d7e840ffaa066d41ac))
* **editor:** Prevent Safari users from accessing the frontend over insecure contexts ([#10510](https://github.com/n8n-io/n8n/issues/10510)) ([a73b9a3](https://github.com/n8n-io/n8n/commit/a73b9a38d6c48e2f78593328e7d9933f2493dbb6))
* **editor:** Scale output item selector input width with value ([#10555](https://github.com/n8n-io/n8n/issues/10555)) ([52c574d](https://github.com/n8n-io/n8n/commit/52c574d83f344f03b0e39984bbc3ac0402e50791))
* **Google Sheets Trigger Node:** Show sheet name is too long error ([#10542](https://github.com/n8n-io/n8n/issues/10542)) ([4e15007](https://github.com/n8n-io/n8n/commit/4e1500757700ec984cdad8b9cfcd76ee00ae127e))
* **Wait Node:** Prevent waiting until invalid date ([#10523](https://github.com/n8n-io/n8n/issues/10523)) ([c0e7620](https://github.com/n8n-io/n8n/commit/c0e7620036738f8d0b382d0d0610b981dcbc29e0))
### Features
* Add new credentials for the HTTP Request node ([#9833](https://github.com/n8n-io/n8n/issues/9833)) ([26f1af3](https://github.com/n8n-io/n8n/commit/26f1af397b2b25e3394fc2dae91a5c281bf33d66))
* **AI Agent Node:** Add tutorial link to agent node ([#10493](https://github.com/n8n-io/n8n/issues/10493)) ([5c7cc36](https://github.com/n8n-io/n8n/commit/5c7cc36c23e58a47a1e71911e7303a1bd54f167e))
* **core:** Expose queue metrics for Prometheus ([#10559](https://github.com/n8n-io/n8n/issues/10559)) ([008c510](https://github.com/n8n-io/n8n/commit/008c510b7623fefb8c60730c7eac54dd9bb2e3fc))
* **editor:** Implement workflowSelector parameter type ([#10482](https://github.com/n8n-io/n8n/issues/10482)) ([84e54be](https://github.com/n8n-io/n8n/commit/84e54beac763f25399c9687f695f1e658e3ce434))
### Performance Improvements
* **core:** Make execution queries faster ([#9817](https://github.com/n8n-io/n8n/issues/9817)) ([dc7dc99](https://github.com/n8n-io/n8n/commit/dc7dc995d5e2ea8fbd0dcb54cfa8aa93ecb437c9))
### Other
* **Add user journey link to [n8n.io](https://n8n.io)** ([#10331](https://github.com/n8n-io/n8n/pull/10331))
# [1.56.0](https://github.com/n8n-io/n8n/compare/n8n@1.55.0...n8n@1.56.0) (2024-08-21)
### Bug Fixes
* Better errors in Switch, If and Filter nodes ([#10457](https://github.com/n8n-io/n8n/issues/10457)) ([aea82cb](https://github.com/n8n-io/n8n/commit/aea82cb74421d516919742127daf669808b57604))
* **Calendly Trigger Node:** Fix issue with webhook url matching ([#10378](https://github.com/n8n-io/n8n/issues/10378)) ([09c3a8b](https://github.com/n8n-io/n8n/commit/09c3a8b36733a9634ef5948922d6aa7a19bbb592))
* **core:** Fix payload property in `workflow-post-execute` event ([#10413](https://github.com/n8n-io/n8n/issues/10413)) ([d98e29e](https://github.com/n8n-io/n8n/commit/d98e29e3d53de87aec276260615fa60473a2692f))
* **core:** Fix XSS validation and separate URL validation ([#10424](https://github.com/n8n-io/n8n/issues/10424)) ([91467ab](https://github.com/n8n-io/n8n/commit/91467ab325e4c71c20c522f3143246d270101626))
* **core:** Replace `sanitize-html` with `xss` in XSS validator constraint ([#10479](https://github.com/n8n-io/n8n/issues/10479)) ([5dea51a](https://github.com/n8n-io/n8n/commit/5dea51aad7d9e7ffc676d16f4bbbdecce5876f0b))
* **core:** Use class-validator with XSS check for survey answers ([#10490](https://github.com/n8n-io/n8n/issues/10490)) ([547a606](https://github.com/n8n-io/n8n/commit/547a60642ce9e54819d4e600c822d87dabd59b2e))
* **core:** Use explicit types in configs to ensure valid decorator metadata ([#10433](https://github.com/n8n-io/n8n/issues/10433)) ([2043daa](https://github.com/n8n-io/n8n/commit/2043daa2570bc04b0b8d41f277901a8cc8a7b98f))
* **editor:** Add workflow scopes when initializing workflow ([#10455](https://github.com/n8n-io/n8n/issues/10455)) ([b857c2c](https://github.com/n8n-io/n8n/commit/b857c2cda0a9e4386a540d5e1e741570d9453588))
* **editor:** Buffer json chunks in stream response ([#10439](https://github.com/n8n-io/n8n/issues/10439)) ([37797f3](https://github.com/n8n-io/n8n/commit/37797f38d81b12d030ba85034baeb49192ea575c))
* **editor:** Fix flaky mapping tests ([#10453](https://github.com/n8n-io/n8n/issues/10453)) ([fc6d413](https://github.com/n8n-io/n8n/commit/fc6d4138d58282f676b32f1a6011b1b6d0184bf2))
* **editor:** Fix overflow in AI Assistant chat messages ([#10491](https://github.com/n8n-io/n8n/issues/10491)) ([4a6ca63](https://github.com/n8n-io/n8n/commit/4a6ca632100731f85875c639f2164bf1ef415009))
* **editor:** Highlight matching type in filter component ([#10425](https://github.com/n8n-io/n8n/issues/10425)) ([6bca879](https://github.com/n8n-io/n8n/commit/6bca879d4ae30c7f9a35e8d6672de42cf93be727))
* **editor:** Show item count in output panel schema view ([#10426](https://github.com/n8n-io/n8n/issues/10426)) ([4dee7cc](https://github.com/n8n-io/n8n/commit/4dee7cc36e5f7768d0b71095b194bf357c92e941))
* **editor:** Truncate long data pill labels in schema view ([#10427](https://github.com/n8n-io/n8n/issues/10427)) ([1bf2f4f](https://github.com/n8n-io/n8n/commit/1bf2f4f6171d666391bb3a3a312468bc083446e3))
* Filter component - improve errors ([#10456](https://github.com/n8n-io/n8n/issues/10456)) ([61ac0c7](https://github.com/n8n-io/n8n/commit/61ac0c77755210f570b887951fe6bbec1a323811))
* **Google Sheets Node:** Better error when column to match on is empty ([#10442](https://github.com/n8n-io/n8n/issues/10442)) ([ce46bf5](https://github.com/n8n-io/n8n/commit/ce46bf516a86d9779f37dd75b0c680d26d88e15d))
* **Google Sheets Node:** Update name and hint for useAppend option ([#10443](https://github.com/n8n-io/n8n/issues/10443)) ([c5a0c04](https://github.com/n8n-io/n8n/commit/c5a0c049eaf44419c690d151de42fb0c10bd406e))
* **Google Sheets Node:** Update to returnAllMatches option ([#10440](https://github.com/n8n-io/n8n/issues/10440)) ([f7fb02e](https://github.com/n8n-io/n8n/commit/f7fb02e92a756781f8e35bbbfc25d71c12cb70af))
* **Invoice Ninja Node:** Fix payment types ([#10462](https://github.com/n8n-io/n8n/issues/10462)) ([129245d](https://github.com/n8n-io/n8n/commit/129245da10be1d645f61e929e40b128bd7813f17))
* **n8n Form Trigger Node:** Show basic authentication modal on wrong credentials ([#10423](https://github.com/n8n-io/n8n/issues/10423)) ([0dc3e99](https://github.com/n8n-io/n8n/commit/0dc3e99b26bec45e747d83f383cfe5169d89e6b7))
* **OpenAI Node:** Throw node operations error in case of openAi client error ([#10448](https://github.com/n8n-io/n8n/issues/10448)) ([0d3ed46](https://github.com/n8n-io/n8n/commit/0d3ed461996bbad06015c455f133baab6506437f))
* Project Viewer always seeing a connection error when testing credentials ([#10417](https://github.com/n8n-io/n8n/issues/10417)) ([613cdd2](https://github.com/n8n-io/n8n/commit/613cdd2ba2c0f224c4857a5fc3eea36dbd683049))
* Remove unimplemented Postgres credentials options ([#10461](https://github.com/n8n-io/n8n/issues/10461)) ([17ac784](https://github.com/n8n-io/n8n/commit/17ac7844f29d819b91dfaf90b9fe386d98060c42))
* Rename Assistant back ([#10481](https://github.com/n8n-io/n8n/issues/10481)) ([c410aed](https://github.com/n8n-io/n8n/commit/c410aed4c22182943dc80ede63acda00b7898e10))
* Require mfa code to change email ([#10354](https://github.com/n8n-io/n8n/issues/10354)) ([39c8e50](https://github.com/n8n-io/n8n/commit/39c8e50ad0513649f5a8cef911b7d6cdd61c2372))
* **Respond to Webhook Node:** Fix issue preventing the chat trigger from working ([#9886](https://github.com/n8n-io/n8n/issues/9886)) ([9d6ad88](https://github.com/n8n-io/n8n/commit/9d6ad88c14a88fd0dfcb4f9981e38d19cf5f3067))
* Show input names when node has multiple inputs ([#10434](https://github.com/n8n-io/n8n/issues/10434)) ([973956c](https://github.com/n8n-io/n8n/commit/973956cc26c78c329ff6eb6934d4f0a24060c87c))
* **Toggl Trigger Node:** Update API version ([#10207](https://github.com/n8n-io/n8n/issues/10207)) ([9bdb1d6](https://github.com/n8n-io/n8n/commit/9bdb1d6dca43fe491c5eb96f093b7eec4509eaff))
### Features
* **core:** Support bidirectional communication between specific mains and specific workers ([#10377](https://github.com/n8n-io/n8n/issues/10377)) ([d0fc9de](https://github.com/n8n-io/n8n/commit/d0fc9dee0e17211c1ed130b19286e9573c9ebfbd))
* **Facebook Graph API Node:** Update node to support API v18 - v20 ([#10419](https://github.com/n8n-io/n8n/issues/10419)) ([e7ee10f](https://github.com/n8n-io/n8n/commit/e7ee10f243663d899d32e61bc6264b4df444e2af))
# [1.55.0](https://github.com/n8n-io/n8n/compare/n8n@1.54.0...n8n@1.55.0) (2024-08-14)
### Bug Fixes
* Add better error handling for chat errors ([#10408](https://github.com/n8n-io/n8n/issues/10408)) ([f82b6e4](https://github.com/n8n-io/n8n/commit/f82b6e4ba9bf527b3a4c17872162d9ae124ead0d))
* **AI Agent Node:** Fix issues with some tools not populating ([#10406](https://github.com/n8n-io/n8n/issues/10406)) ([51a1edd](https://github.com/n8n-io/n8n/commit/51a1eddbf00393f3881c340cf37cfcca59566c99))
* **core:** Account for cancelling an execution with no workers available ([#10343](https://github.com/n8n-io/n8n/issues/10343)) ([b044e78](https://github.com/n8n-io/n8n/commit/b044e783e73a499dbd7532a5d489a782d3d021da))
* **core:** Account for owner when filtering by project ID in `GET /workflows` in Public API ([#10379](https://github.com/n8n-io/n8n/issues/10379)) ([5ac65b3](https://github.com/n8n-io/n8n/commit/5ac65b36bcb1351c6233b951f064f60862f790a5))
* **core:** Enforce shutdown timer and sequence on `SIGINT` for main ([#10346](https://github.com/n8n-io/n8n/issues/10346)) ([5255793](https://github.com/n8n-io/n8n/commit/5255793afee5653d8356b8e4d2e1009d5cf36164))
* **core:** Filter out prototype and constructor lookups in expressions ([#10382](https://github.com/n8n-io/n8n/issues/10382)) ([8e7d29a](https://github.com/n8n-io/n8n/commit/8e7d29ad3c4872b1cc147dfcfe9a864ba916692f))
* **core:** Fix duplicate Redis publisher ([#10392](https://github.com/n8n-io/n8n/issues/10392)) ([45813de](https://github.com/n8n-io/n8n/commit/45813debc963096f63cc0aabe82d9d9f853a39d7))
* **core:** Fix worker shutdown errors when active executions ([#10353](https://github.com/n8n-io/n8n/issues/10353)) ([e071b73](https://github.com/n8n-io/n8n/commit/e071b73bab34edd4b3e6aef6497514acc504cdc6))
* **core:** Prevent XSS in user update endpoints ([#10338](https://github.com/n8n-io/n8n/issues/10338)) ([7898498](https://github.com/n8n-io/n8n/commit/78984986a6b4add89df9743b94c113046f1d5ee8))
* **core:** Prevent XSS via static cache dir ([#10339](https://github.com/n8n-io/n8n/issues/10339)) ([4f392b5](https://github.com/n8n-io/n8n/commit/4f392b5e3e0ee166e85a2e060b3ec7fcf145229b))
* **core:** Rate limit MFA activation and verification endpoints ([#10330](https://github.com/n8n-io/n8n/issues/10330)) ([b6c47c0](https://github.com/n8n-io/n8n/commit/b6c47c0e3214878d42980d5c9535df52b3984b3c))
* **editor:** Connect up new project viewer role to the FE ([#9913](https://github.com/n8n-io/n8n/issues/9913)) ([117e2d9](https://github.com/n8n-io/n8n/commit/117e2d968fcc535f32c583ac8f2dc8a84e8cd6bd))
* **editor:** Enable credential sharing between all types of projects ([#10233](https://github.com/n8n-io/n8n/issues/10233)) ([1cf48cc](https://github.com/n8n-io/n8n/commit/1cf48cc3019c1cf27e2f3c9affd18426237e9064))
* **editor:** Fix rendering of SVG icons in public chat on iOS ([#10381](https://github.com/n8n-io/n8n/issues/10381)) ([7ab3811](https://github.com/n8n-io/n8n/commit/7ab38114dbf3881afba39287a061446ec4bf0431))
* **editor:** Fixing XSS vulnerability in toast messages ([#10329](https://github.com/n8n-io/n8n/issues/10329)) ([38bdd9f](https://github.com/n8n-io/n8n/commit/38bdd9f5d0d9ca06fab1a7e1a3e7a4a648a6a89a))
* **editor:** Revert change that hid swagger docs in the ui ([#10350](https://github.com/n8n-io/n8n/issues/10350)) ([bae49d3](https://github.com/n8n-io/n8n/commit/bae49d3198d4bcc27e7996cd4f7be3132becc98e))
* **n8n Form Trigger Node:** Fix issue preventing v1 node from working ([#10364](https://github.com/n8n-io/n8n/issues/10364)) ([9b647a9](https://github.com/n8n-io/n8n/commit/9b647a9837434e8b75e3ad754ff5136bb5ac760d))
* Require mfa code for password change if its enabled ([#10341](https://github.com/n8n-io/n8n/issues/10341)) ([9d7caac](https://github.com/n8n-io/n8n/commit/9d7caacc699f10962783393925a980ec6f1ca975))
* Require mfa code to disable mfa ([#10345](https://github.com/n8n-io/n8n/issues/10345)) ([3384f52](https://github.com/n8n-io/n8n/commit/3384f52a35b835ba1d8633dc94bab0ad6e7023b3))
### Features
* Add Ask assistant behind feature flag ([#9995](https://github.com/n8n-io/n8n/issues/9995)) ([5ed2a77](https://github.com/n8n-io/n8n/commit/5ed2a77740db1f02b27c571f4dfdfa206ebdb19c))
* **AI Transform Node:** New node ([#10405](https://github.com/n8n-io/n8n/issues/10405)) ([4d222ac](https://github.com/n8n-io/n8n/commit/4d222ac19d943b69fd9f87abe5e5c5f5141eed8d))
* **AI Transform Node:** New node ([#9990](https://github.com/n8n-io/n8n/issues/9990)) ([0de9d56](https://github.com/n8n-io/n8n/commit/0de9d56619ed1c055407353046b8a9ebe78da527))
* **core:** Allow overriding npm registry for community packages ([#10325](https://github.com/n8n-io/n8n/issues/10325)) ([33a2703](https://github.com/n8n-io/n8n/commit/33a2703429d9eaa41f72d2e7d2da5be60b6c620f))
* **editor:** Add schema view to expression modal ([#9976](https://github.com/n8n-io/n8n/issues/9976)) ([71b6c67](https://github.com/n8n-io/n8n/commit/71b6c671797024d7b516352fa9b7ecda101ce3b2))
* **MySQL Node:** Return decimal types as numbers ([#10313](https://github.com/n8n-io/n8n/issues/10313)) ([f744d7c](https://github.com/n8n-io/n8n/commit/f744d7c100be68669d9a3efd0033dd371a3cfaf7))
* **Okta Node:** Add Okta Node ([#10278](https://github.com/n8n-io/n8n/issues/10278)) ([5cac0f3](https://github.com/n8n-io/n8n/commit/5cac0f339d649cfe5857d33738210cbc1599370b))
# [1.54.0](https://github.com/n8n-io/n8n/compare/n8n@1.53.0...n8n@1.54.0) (2024-08-07)
### Bug Fixes
* **core:** Ensure OAuth token data is not stubbed in source control ([#10302](https://github.com/n8n-io/n8n/issues/10302)) ([98115e9](https://github.com/n8n-io/n8n/commit/98115e95df8289a8ec400a570a7f256382f8e286))
* **core:** Fix expressions in webhook nodes(Form, Webhook) to access previous node's data ([#10247](https://github.com/n8n-io/n8n/issues/10247)) ([88a1701](https://github.com/n8n-io/n8n/commit/88a170176a3447e7f847e9cf145aeb867b1c5fcf))
* **core:** Fix user telemetry bugs ([#10293](https://github.com/n8n-io/n8n/issues/10293)) ([42a0b59](https://github.com/n8n-io/n8n/commit/42a0b594d6ea2527c55a2aa9976c904cf70ecf92))
* **core:** Make execution and its data creation atomic ([#10276](https://github.com/n8n-io/n8n/issues/10276)) ([ae50bb9](https://github.com/n8n-io/n8n/commit/ae50bb95a8e5bf1cdbf9483da54b84094b82e260))
* **core:** Make OAuth1/OAuth2 callback not require auth ([#10263](https://github.com/n8n-io/n8n/issues/10263)) ([a8e2774](https://github.com/n8n-io/n8n/commit/a8e2774f5382e202556b5506c7788265786aa973))
* **core:** Revert transactions until we remove the legacy sqlite driver ([#10299](https://github.com/n8n-io/n8n/issues/10299)) ([1eba7c3](https://github.com/n8n-io/n8n/commit/1eba7c3c763ac5b6b28c1c6fc43fc8c215249292))
* **core:** Surface enterprise trial error message ([#10267](https://github.com/n8n-io/n8n/issues/10267)) ([432ac1d](https://github.com/n8n-io/n8n/commit/432ac1da59e173ce4c0f2abbc416743d9953ba70))
* **core:** Upgrade tournament to address some XSS vulnerabilities ([#10277](https://github.com/n8n-io/n8n/issues/10277)) ([43ae159](https://github.com/n8n-io/n8n/commit/43ae159ea40c574f8e41bdfd221ab2bf3268eee7))
* **core:** VM2 sandbox should not throw on `new Promise` ([#10298](https://github.com/n8n-io/n8n/issues/10298)) ([7e95f9e](https://github.com/n8n-io/n8n/commit/7e95f9e2e40a99871f1b6abcdacb39ac5f857332))
* **core:** Webhook and form baseUrl missing ([#10290](https://github.com/n8n-io/n8n/issues/10290)) ([8131d66](https://github.com/n8n-io/n8n/commit/8131d66f8ca1b1da00597a12859ee4372148a0c9))
* **editor:** Enable moving resources only if team projects are available by the license ([#10271](https://github.com/n8n-io/n8n/issues/10271)) ([42ba884](https://github.com/n8n-io/n8n/commit/42ba8841c401126c77158a53dc8fcbb45dfce8fd))
* **editor:** Fix execution retry button ([#10275](https://github.com/n8n-io/n8n/issues/10275)) ([55f2ffe](https://github.com/n8n-io/n8n/commit/55f2ffe256c91a028cee95c3bbb37a093a1c0f81))
* **editor:** Update design system Avatar component to show initials also when only firstName or lastName is given ([#10308](https://github.com/n8n-io/n8n/issues/10308)) ([46bbf09](https://github.com/n8n-io/n8n/commit/46bbf09beacad12472d91786b91d845fe2afb26d))
* **editor:** Update tags filter/editor to not show non existing tag as a selectable option ([#10297](https://github.com/n8n-io/n8n/issues/10297)) ([557a76e](https://github.com/n8n-io/n8n/commit/557a76ec2326de72fb7a8b46fc4353f8fd9b591d))
* **Invoice Ninja Node:** Fix payment types ([#10196](https://github.com/n8n-io/n8n/issues/10196)) ([c5acbb7](https://github.com/n8n-io/n8n/commit/c5acbb7ec0d24ec9b30c221fa3b2fb615fb9ec7f))
* Loop node no input data shown ([#10224](https://github.com/n8n-io/n8n/issues/10224)) ([c8ee852](https://github.com/n8n-io/n8n/commit/c8ee852159207be0cfe2c3e0ee8e7b29d838aa35))
### Features
* **core:** Allow filtering executions and users by project in Public API ([#10250](https://github.com/n8n-io/n8n/issues/10250)) ([7056e50](https://github.com/n8n-io/n8n/commit/7056e50b006bda665f64ce6234c5c1967891c415))
* **core:** Allow transferring credentials in Public API ([#10259](https://github.com/n8n-io/n8n/issues/10259)) ([07d7b24](https://github.com/n8n-io/n8n/commit/07d7b247f02a9d7185beca7817deb779a3d665dd))
* **core:** Show sub-node error on the logs pane. Open logs pane on sub-node error ([#10248](https://github.com/n8n-io/n8n/issues/10248)) ([57d1c9a](https://github.com/n8n-io/n8n/commit/57d1c9a99e97308f2f1b8ae05ac3861a835e8e5a))
* **core:** Support community packages in scaling-mode ([#10228](https://github.com/n8n-io/n8n/issues/10228)) ([88086a4](https://github.com/n8n-io/n8n/commit/88086a41ff5b804b35aa9d9503dc2d48836fe4ec))
* **core:** Support create, delete, edit role for users in Public API ([#10279](https://github.com/n8n-io/n8n/issues/10279)) ([84efbd9](https://github.com/n8n-io/n8n/commit/84efbd9b9c51f536b21a4f969ab607d277bef692))
* **core:** Support create, read, update, delete projects in Public API ([#10269](https://github.com/n8n-io/n8n/issues/10269)) ([489ce10](https://github.com/n8n-io/n8n/commit/489ce100634c3af678fb300e9a39d273042542e6))
* **editor:** Auto-add LLM chain for new LLM nodes on empty canvas ([#10245](https://github.com/n8n-io/n8n/issues/10245)) ([06419d9](https://github.com/n8n-io/n8n/commit/06419d9483ae916e79aace6d8c17e265b419b15d))
* **Elasticsearch Node:** Add bulk operations for Elasticsearch ([#9940](https://github.com/n8n-io/n8n/issues/9940)) ([bf8f848](https://github.com/n8n-io/n8n/commit/bf8f848645dfd31527713a55bd1fc93865327017))
* **Lemlist Trigger Node:** Update Trigger events ([#10311](https://github.com/n8n-io/n8n/issues/10311)) ([15f10ec](https://github.com/n8n-io/n8n/commit/15f10ec325cb5eda0f952bed3a5f171dd91bc639))
* **MongoDB Node:** Add projection to query options on Find ([#9972](https://github.com/n8n-io/n8n/issues/9972)) ([0a84e0d](https://github.com/n8n-io/n8n/commit/0a84e0d8b047669f5cf023c21383d01c929c5b4f))
* **Postgres Chat Memory, Redis Chat Memory, Xata:** Add support for context window length ([#10203](https://github.com/n8n-io/n8n/issues/10203)) ([e3edeaa](https://github.com/n8n-io/n8n/commit/e3edeaa03526f041d15d1099ea91869e38a0decc))
* **Stripe Trigger Node:** Add Stripe webhook descriptions based on the workflow ID and name ([#9956](https://github.com/n8n-io/n8n/issues/9956)) ([3433465](https://github.com/n8n-io/n8n/commit/34334651e0e6874736a437a894176bed4590e5a7))
* **Webflow Node:** Update to use the v2 API ([#9996](https://github.com/n8n-io/n8n/issues/9996)) ([6d8323f](https://github.com/n8n-io/n8n/commit/6d8323fadea8af04483eb1a873df0cf3ccc2a891))
# [1.53.0](https://github.com/n8n-io/n8n/compare/n8n@1.52.0...n8n@1.53.0) (2024-07-31)
### Bug Fixes
* Better error message when calling data transformation functions on a null value ([#10210](https://github.com/n8n-io/n8n/issues/10210)) ([1718125](https://github.com/n8n-io/n8n/commit/1718125c6d8589cf24dc8d34f6808dd6f1802691))
* **core:** Fix missing successful items on continueErrorOutput with multiple outputs ([#10218](https://github.com/n8n-io/n8n/issues/10218)) ([1a7713e](https://github.com/n8n-io/n8n/commit/1a7713ef263680da43f08b6c8a15aee7a0341493))
* **core:** Flush instance stopped event immediately ([#10238](https://github.com/n8n-io/n8n/issues/10238)) ([d6770b5](https://github.com/n8n-io/n8n/commit/d6770b5fcaec6438d677b918aaeb1669ad7424c2))
* **core:** Restore log event `n8n.workflow.failed` ([#10253](https://github.com/n8n-io/n8n/issues/10253)) ([3e96b29](https://github.com/n8n-io/n8n/commit/3e96b293329525c9d4b2fcef87b3803e458c8e7f))
* **core:** Upgrade @n8n/vm2 to address CVE202337466 ([#10265](https://github.com/n8n-io/n8n/issues/10265)) ([2a09a03](https://github.com/n8n-io/n8n/commit/2a09a036d2e916acff7ee50904f1d011a93758e1))
* **editor:** Defer `User saved credentials` telemetry event for OAuth credentials ([#10215](https://github.com/n8n-io/n8n/issues/10215)) ([40a5226](https://github.com/n8n-io/n8n/commit/40a5226e24448a4428143e69d80ebc78238365a1))
* **editor:** Fix custom API call notice ([#10227](https://github.com/n8n-io/n8n/issues/10227)) ([5b47c8b](https://github.com/n8n-io/n8n/commit/5b47c8b57b25528cd2d6f97bc6d98707d47f35bc))
* **editor:** Fix issue with existing credential not opening in HTTP agent tool ([#10167](https://github.com/n8n-io/n8n/issues/10167)) ([906b4c3](https://github.com/n8n-io/n8n/commit/906b4c3c7b2919111cf23eaa12b3c4d507969179))
* **editor:** Fix parameter input glitch when there was an error loading remote options ([#10209](https://github.com/n8n-io/n8n/issues/10209)) ([c0e3743](https://github.com/n8n-io/n8n/commit/c0e37439a87105a0e66c8ebced42c06dab30dc5e))
* **editor:** Fix workflow execution list scrolling after filter change ([#10226](https://github.com/n8n-io/n8n/issues/10226)) ([7e64358](https://github.com/n8n-io/n8n/commit/7e643589c67adc0218216ec4b89a95f0edfedbee))
* **Google BigQuery Node:** Send timeoutMs in query, pagination support ([#10205](https://github.com/n8n-io/n8n/issues/10205)) ([f5722e8](https://github.com/n8n-io/n8n/commit/f5722e8823ccd2bc2b5f43ba3c849797d5690a93))
* **Google Sheets Node:** Add column names row if sheet is empty ([#10200](https://github.com/n8n-io/n8n/issues/10200)) ([82eba9f](https://github.com/n8n-io/n8n/commit/82eba9fc5ff49b8e2a9db93c10b253fb67a8c644))
* **Google Sheets Node:** Do not insert row_number as a new column, do not checkForSchemaChanges in update operation ([#10201](https://github.com/n8n-io/n8n/issues/10201)) ([5136d10](https://github.com/n8n-io/n8n/commit/5136d10ca3492f92af67d4a1d4abc774419580cc))
* **Google Sheets Node:** Fix Google Sheet URL regex ([#10195](https://github.com/n8n-io/n8n/issues/10195)) ([e6fd996](https://github.com/n8n-io/n8n/commit/e6fd996973d4f40facf0ebf1eea3cc26acd0603d))
* **HTTP Request Node:** Resolve max pages expression ([#10192](https://github.com/n8n-io/n8n/issues/10192)) ([bfc8e1b](https://github.com/n8n-io/n8n/commit/bfc8e1b56f7714e1f52aae747d58d686b86e60f0))
* **LinkedIn Node:** Fix issue with some characters cutting off posts early ([#10185](https://github.com/n8n-io/n8n/issues/10185)) ([361b5e7](https://github.com/n8n-io/n8n/commit/361b5e7c37ba49b68dcf5b8122621aad4d8d96e0))
* **Postgres Node:** Expressions in query parameters for Postgres executeQuery operation ([#10217](https://github.com/n8n-io/n8n/issues/10217)) ([519fc4d](https://github.com/n8n-io/n8n/commit/519fc4d75325a80b84cc4dcacf52d6f4c02e3a44))
* **Postgres Node:** Option to treat query parameters enclosed in single quotas as text ([#10214](https://github.com/n8n-io/n8n/issues/10214)) ([00ec253](https://github.com/n8n-io/n8n/commit/00ec2533374d3def465efee718592fc4001d5602))
* **Read/Write Files from Disk Node:** Notice update in file selector, replace backslashes with forward slashes if windows path ([#10186](https://github.com/n8n-io/n8n/issues/10186)) ([3eac673](https://github.com/n8n-io/n8n/commit/3eac673b17986c5c74bd2adb5ad589ba0ca55319))
* **Text Classifier Node:** Use proper documentation URL and respect continueOnFail ([#10216](https://github.com/n8n-io/n8n/issues/10216)) ([452f52c](https://github.com/n8n-io/n8n/commit/452f52c124017e002e86c547ba42b1633b14beed))
* **Trello Node:** Use body for POST requests ([#10189](https://github.com/n8n-io/n8n/issues/10189)) ([7775d50](https://github.com/n8n-io/n8n/commit/7775d5059b7f69d9af22e7ad7d12c6cf9092a4e5))
* **Wait Node:** Authentication fix ([#10236](https://github.com/n8n-io/n8n/issues/10236)) ([f87854f](https://github.com/n8n-io/n8n/commit/f87854f8db360b7b870583753fcfb4af95adab8c))
### Features
* **Calendly Trigger Node:** Add OAuth Credentials Support ([#10251](https://github.com/n8n-io/n8n/issues/10251)) ([326c983](https://github.com/n8n-io/n8n/commit/326c983915a2c382e32398358e7dcadd022c0b77))
* **core:** Allow filtering workflows by project and transferring workflows in Public API ([#10231](https://github.com/n8n-io/n8n/issues/10231)) ([d719899](https://github.com/n8n-io/n8n/commit/d719899223907b20a17883a35e4ef637a3453532))
* **editor:** Show new executions as `Queued` in the UI, until they actually start ([#10204](https://github.com/n8n-io/n8n/issues/10204)) ([44728d7](https://github.com/n8n-io/n8n/commit/44728d72423f5549dda09589f4a618ebd80899cb))
* **HTTP Request Node:** Add option to disable lowercase headers ([#10154](https://github.com/n8n-io/n8n/issues/10154)) ([5aba69b](https://github.com/n8n-io/n8n/commit/5aba69bcf4d232d9860f3cd9fe57cb8839a2f96f))
* **Information Extractor Node:** Add new simplified AI-node for information extraction ([#10149](https://github.com/n8n-io/n8n/issues/10149)) ([3d235b0](https://github.com/n8n-io/n8n/commit/3d235b0b2df756df35ac60e3dcd87ad183a07167))
* Introduce Google Cloud Platform as external secrets provider ([#10146](https://github.com/n8n-io/n8n/issues/10146)) ([3ccb9df](https://github.com/n8n-io/n8n/commit/3ccb9df2f902e46f8cbb9c46c0727f29d752a773))
* **n8n Form Trigger Node:** Improvements ([#10092](https://github.com/n8n-io/n8n/issues/10092)) ([711b667](https://github.com/n8n-io/n8n/commit/711b667ebefe55740e5eb39f1f0f24ceee10e7b0))
* Recovery option for jsonParse helper ([#10182](https://github.com/n8n-io/n8n/issues/10182)) ([d165b33](https://github.com/n8n-io/n8n/commit/d165b33ceac4d24d0fc290bffe63b5f551204e38))
* **Sentiment Analysis Node:** Implement Sentiment Analysis node ([#10184](https://github.com/n8n-io/n8n/issues/10184)) ([8ef0a0c](https://github.com/n8n-io/n8n/commit/8ef0a0c58ac2a84aad649ccbe72aa907d005cc44))
* **Shopify Node:** Update Shopify API version ([#10155](https://github.com/n8n-io/n8n/issues/10155)) ([e2ee915](https://github.com/n8n-io/n8n/commit/e2ee91569a382bfbf787cf45204c72c821a860a0))
* Support create, read, delete variables in Public API ([#10241](https://github.com/n8n-io/n8n/issues/10241)) ([af695eb](https://github.com/n8n-io/n8n/commit/af695ebf934526d926ea87fe87df61aa73d70979))
# [1.52.0](https://github.com/n8n-io/n8n/compare/n8n@1.51.0...n8n@1.52.0) (2024-07-24)
### Bug Fixes
* **core:** Fix handling of common events for relays ([#10135](https://github.com/n8n-io/n8n/issues/10135)) ([d2a3a4a](https://github.com/n8n-io/n8n/commit/d2a3a4a080cdcc04f50fa33fd81d361efce3f709))
* **core:** Fix SSH Tunnels when using private key ([#10148](https://github.com/n8n-io/n8n/issues/10148)) ([a96db34](https://github.com/n8n-io/n8n/commit/a96db344e54658787426d967dfa299c7a6dd14e7))
* **core:** Metadata inserts using existing IDs and failing with postgres ([#10108](https://github.com/n8n-io/n8n/issues/10108)) ([4547a49](https://github.com/n8n-io/n8n/commit/4547a49db15a20f5f147e859b6c2c01f60f9565c))
* **core:** Respect prefix for all Prometheus metrics ([#10130](https://github.com/n8n-io/n8n/issues/10130)) ([b1816db](https://github.com/n8n-io/n8n/commit/b1816db449ed451443f353b69166b7ca700ba51e))
* **core:** Support branches containing slashes in source control ([#10109](https://github.com/n8n-io/n8n/issues/10109)) ([03a833d](https://github.com/n8n-io/n8n/commit/03a833db51a25dda6cf0d8494f06c6704f6f3c7f))
* **core:** Support execution recovery when saving execution progress ([#10104](https://github.com/n8n-io/n8n/issues/10104)) ([d887c82](https://github.com/n8n-io/n8n/commit/d887c82d808a79babc726fc789cc014194ae2ac6))
* **editor:** Allow `$secrets` to resolve on credentials ([#10093](https://github.com/n8n-io/n8n/issues/10093)) ([bf57f38](https://github.com/n8n-io/n8n/commit/bf57f38d1c417ba8b20144934c8e97a75c1f51cc))
* **editor:** Fix saving and connecting on LDAP setup form ([#10163](https://github.com/n8n-io/n8n/issues/10163)) ([30784fb](https://github.com/n8n-io/n8n/commit/30784fb76cec790a782fae40973a956a8d81c0b2))
* **editor:** Fix updating/uninstalling community nodes ([#10138](https://github.com/n8n-io/n8n/issues/10138)) ([de015ff](https://github.com/n8n-io/n8n/commit/de015ff2978a5ee3345449626025c6d0793b6f5a))
* **editor:** Remove "move" action from workflow and credential on community plan ([#10057](https://github.com/n8n-io/n8n/issues/10057)) ([5a9a271](https://github.com/n8n-io/n8n/commit/5a9a2713b499cc7dcddb500a54e24bbf7145b504))
* **editor:** UX Improvements to RBAC feature set ([#9683](https://github.com/n8n-io/n8n/issues/9683)) ([028a8a2](https://github.com/n8n-io/n8n/commit/028a8a2c754e4f6d6a5f0918a656eb4554eb869f))
* **HelpScout Node:** Fix issue with thread types not working correctly ([#10084](https://github.com/n8n-io/n8n/issues/10084)) ([68d3beb](https://github.com/n8n-io/n8n/commit/68d3bebfeebea9054bbbaebac31c2e3fa34336bb))
* **MQTT Node:** Node hangs forever on failed connection ([#10048](https://github.com/n8n-io/n8n/issues/10048)) ([76c2906](https://github.com/n8n-io/n8n/commit/76c290655de7d4e626725a05fd991a0858cca0d7))
* **n8n Form Trigger Node:** Execution from canvas ([#10132](https://github.com/n8n-io/n8n/issues/10132)) ([b07c5e2](https://github.com/n8n-io/n8n/commit/b07c5e201165165c4e91ddd19b6fa79703ba2a9c))
* **Notion Node:** Fix issue preventing some database page urls from working ([#10070](https://github.com/n8n-io/n8n/issues/10070)) ([7848c19](https://github.com/n8n-io/n8n/commit/7848c19f543d5f5f62b89cc5644639c6afdb8fa6))
* **RabbitMQ Node:** Fix issue with arguments not being sent ([#9397](https://github.com/n8n-io/n8n/issues/9397)) ([1c666e6](https://github.com/n8n-io/n8n/commit/1c666e6e7c2be2e2d0dcc528870fddfa8b02318b))
### Features
* **editor:** Split Tools and Models into sub-sections ([#10159](https://github.com/n8n-io/n8n/issues/10159)) ([3846eb9](https://github.com/n8n-io/n8n/commit/3846eb967afd77dba6f037e8185ed94494454d5a))
* Introduce Azure Key Vault as external secrets provider ([#10054](https://github.com/n8n-io/n8n/issues/10054)) ([1b6c2d3](https://github.com/n8n-io/n8n/commit/1b6c2d3a37a78ed07ada93be2a57e4b7f7149e58))
* **Pinecone Vector Store Node, Supabase Vector Store Node:** Add update operation to vector store nodes ([#10060](https://github.com/n8n-io/n8n/issues/10060)) ([7e1eeb4](https://github.com/n8n-io/n8n/commit/7e1eeb4c31d3f25ec31baa7390b11a7e3280ce01))
* **Send Email Node:** Smtp credential improvements ([#10147](https://github.com/n8n-io/n8n/issues/10147)) ([dc13ceb](https://github.com/n8n-io/n8n/commit/dc13ceb41649eab42ef073247f3b52c040826e98))
# [1.51.0](https://github.com/n8n-io/n8n/compare/n8n@1.50.0...n8n@1.51.0) (2024-07-17)
### Bug Fixes
* **AMQP Sender Node:** Node hangs forever on disconnect ([#10026](https://github.com/n8n-io/n8n/issues/10026)) ([27410ab](https://github.com/n8n-io/n8n/commit/27410ab2af87573045f38e14e7e20bedd3b0365d))
* **AMQP Trigger Node:** Manual execution updated error reduced wait time ([#10035](https://github.com/n8n-io/n8n/issues/10035)) ([f78f4ea](https://github.com/n8n-io/n8n/commit/f78f4ea3492560bc7056023fd0276990f3ac9b00))
* **AWS Comprehend Node:** Add paired item support ([#10015](https://github.com/n8n-io/n8n/issues/10015)) ([470d496](https://github.com/n8n-io/n8n/commit/470d4966c67a3e4155d59e6fadab467b73134ec4))
* **core:** Ensure executions cannot resume if already running ([#10014](https://github.com/n8n-io/n8n/issues/10014)) ([d651be4](https://github.com/n8n-io/n8n/commit/d651be4e01a869a6f7d70e691e0f5e244f59490e))
* **core:** Redact `csrfSecret` when returning oauth credentials to the frontend ([#10075](https://github.com/n8n-io/n8n/issues/10075)) ([48f047e](https://github.com/n8n-io/n8n/commit/48f047ee2ecbfbd364151816df5fc21e09ca72a6))
* **core:** Stopping an execution should reject any response promises ([#9992](https://github.com/n8n-io/n8n/issues/9992)) ([36b314d](https://github.com/n8n-io/n8n/commit/36b314d0311ef84f275efbc20997c6a77db81b31))
* **editor:** Ensure all static assets are accessible from the server ([#10062](https://github.com/n8n-io/n8n/issues/10062)) ([3bde845](https://github.com/n8n-io/n8n/commit/3bde8453efa9a4d14404c63bdc061c87843d49d2))
* **editor:** Handle disabled nodes in schema view ([#10052](https://github.com/n8n-io/n8n/issues/10052)) ([ab5688c](https://github.com/n8n-io/n8n/commit/ab5688c582c05afd7d3e0967eda0f5dc73d6d3ed))
* **editor:** Make schema view use the correct output ([#10016](https://github.com/n8n-io/n8n/issues/10016)) ([c29664d](https://github.com/n8n-io/n8n/commit/c29664d68851ec33e4d810fa24aba72bb6cecc86))
* **editor:** Provide autocomplete for nodes, even when intermediate node has not run ([#10036](https://github.com/n8n-io/n8n/issues/10036)) ([46d6edc](https://github.com/n8n-io/n8n/commit/46d6edc2a4edd49ae58c0c60977809554e07f4ee))
* **editor:** Remove push event listeners when migrating away from the canvas ([#10063](https://github.com/n8n-io/n8n/issues/10063)) ([0d12f0a](https://github.com/n8n-io/n8n/commit/0d12f0a6b36aaaae5e1f9fab8ad73feeba9ec5ed))
* **editor:** Use selected input item for autocomplete ([#10019](https://github.com/n8n-io/n8n/issues/10019)) ([1d2b403](https://github.com/n8n-io/n8n/commit/1d2b403644278fa6158272edc4295d4565554e37))
* **Email Trigger (IMAP) Node:** Reconnect not working correctly ([#10064](https://github.com/n8n-io/n8n/issues/10064)) ([68d5d7e](https://github.com/n8n-io/n8n/commit/68d5d7e2e90ede5d021a12304dd665247dde5243))
* Filter component - array contains comparison not correct when ignore case option set to true ([#10012](https://github.com/n8n-io/n8n/issues/10012)) ([4a3b97c](https://github.com/n8n-io/n8n/commit/4a3b97cede531adbf81274c1ec2ce4ee400cb48e))
* **GitHub Node:** File Create operation prevent duplicated base64 encoding ([#10040](https://github.com/n8n-io/n8n/issues/10040)) ([9bcc926](https://github.com/n8n-io/n8n/commit/9bcc926a91d7afab0c2ef6eb57e818ef79e3a8f7))
* **HTTP Request Node:** Respect the original encoding of the incoming response ([#9869](https://github.com/n8n-io/n8n/issues/9869)) ([2d19aef](https://github.com/n8n-io/n8n/commit/2d19aef54083d97e94e50a1ee58e8525bbf28548))
* HTTP Request tool - allow hyphens in placeholders ([#10037](https://github.com/n8n-io/n8n/issues/10037)) ([8cd9370](https://github.com/n8n-io/n8n/commit/8cd93704bee116eceb0e3bd5fa849c4b314454ec))
* HTTP Request tool - do not error on missing headers ([#10044](https://github.com/n8n-io/n8n/issues/10044)) ([04b62e0](https://github.com/n8n-io/n8n/commit/04b62e0398eafd923d5f27a3e1c71b925ddb8817))
* **HubSpot Node:** Migrate from v2 owners api ([#10013](https://github.com/n8n-io/n8n/issues/10013)) ([56dd491](https://github.com/n8n-io/n8n/commit/56dd491bcaeab1d11d7874f190eaf20d2e315ca1))
* Number input defaults to 0 not allowing to have arbitrary precision ([#10021](https://github.com/n8n-io/n8n/issues/10021)) ([e4e66ab](https://github.com/n8n-io/n8n/commit/e4e66ab7da5651fede8b3065419ffb393a2fd16d))
* **OpenAI Chat Model Node:** Respect baseURL override for /models ([#10076](https://github.com/n8n-io/n8n/issues/10076)) ([e5dda57](https://github.com/n8n-io/n8n/commit/e5dda5731dfbb50f5aaf2b152f9c5bc89b1d80a6))
* **Telegram Trigger Node:** Fix issue with videos not being downloaded ([#10007](https://github.com/n8n-io/n8n/issues/10007)) ([e84ab35](https://github.com/n8n-io/n8n/commit/e84ab35c4ab0ec47bdbd4343e58c62bbb70f3ec9))
* **Webhook Node:** Binary property option name and description update ([#10043](https://github.com/n8n-io/n8n/issues/10043)) ([9302e33](https://github.com/n8n-io/n8n/commit/9302e33d558564bb5ba172eaeb8c300693b87286))
### Features
* **Asana Node:** Add support for project privacy settings ([#10027](https://github.com/n8n-io/n8n/issues/10027)) ([429481c](https://github.com/n8n-io/n8n/commit/429481c5c4b7f448739a561596873038185ba467))
* Better error when calling expression function on input that is undefined or null ([#10009](https://github.com/n8n-io/n8n/issues/10009)) ([519e57b](https://github.com/n8n-io/n8n/commit/519e57bda5115149357fb2b1c2270e481ea09e38))
* **editor:** Make expression autocomplete search case-insensitive ([#10017](https://github.com/n8n-io/n8n/issues/10017)) ([cde6fe9](https://github.com/n8n-io/n8n/commit/cde6fe90e5c8a9c5983e27f0d82599425fba915b))
* **editor:** Tweak node creator search logic for AI sub-nodes ([#10025](https://github.com/n8n-io/n8n/issues/10025)) ([7db1656](https://github.com/n8n-io/n8n/commit/7db16561dc890849e2d5742bb73f9d5b8e79e37d))
* **Google Vertex Chat Model Node:** Add support for Google Vertex AI Chat models ([#9970](https://github.com/n8n-io/n8n/issues/9970)) ([071130a](https://github.com/n8n-io/n8n/commit/071130a2dc0b450eb6ce6d39fe28cfeefd05633c))
* **Postgres Chat Memory Node:** Implement Postgres Chat Memory node ([#10071](https://github.com/n8n-io/n8n/issues/10071)) ([9cbbb63](https://github.com/n8n-io/n8n/commit/9cbbb6335df0d36f66f22c18041d12f14dc59b32))
* **Text Classifier Node:** Add Text Classifier Node ([#9997](https://github.com/n8n-io/n8n/issues/9997)) ([28ca7d6](https://github.com/n8n-io/n8n/commit/28ca7d6a2dd818c8795acda6ddf7329b8621d9de))
# [1.50.0](https://github.com/n8n-io/n8n/compare/n8n@1.49.0...n8n@1.50.0) (2024-07-10)
### Bug Fixes
* **core:** Aborting manual trigger tests should call `closeFunction` ([#9980](https://github.com/n8n-io/n8n/issues/9980)) ([6107798](https://github.com/n8n-io/n8n/commit/61077985163037ed3c6a8e9e7476cd6c525ff5f2))
* **core:** Allow owner and admin to edit nodes with credentials that haven't been shared with them explicitly ([#9922](https://github.com/n8n-io/n8n/issues/9922)) ([0f49598](https://github.com/n8n-io/n8n/commit/0f495986f89b60ec9bb86801f9779ee9aa87ccfb))
* **core:** Clear active execution on cancellation in scaling mode ([#9979](https://github.com/n8n-io/n8n/issues/9979)) ([7e972c7](https://github.com/n8n-io/n8n/commit/7e972c78afaf950effec17d8eee16cbf86101d03))
* **core:** Disconnect Redis after pausing queue during worker shutdown ([#9928](https://github.com/n8n-io/n8n/issues/9928)) ([c82579b](https://github.com/n8n-io/n8n/commit/c82579bf760cc4b5a2670b14e4e48fc37e2e2263))
* **core:** Don't execute 'workflowExecuteBefore' hook on execution continuations ([#9905](https://github.com/n8n-io/n8n/issues/9905)) ([adb8315](https://github.com/n8n-io/n8n/commit/adb83155ca9478a548e6fe926735d5872de10fea))
* **core:** Prevent multiple values in the execution metadata for the same key and executionId ([#9953](https://github.com/n8n-io/n8n/issues/9953)) ([2e6b03b](https://github.com/n8n-io/n8n/commit/2e6b03b2cb471aefa8104b7b80cf12e64f16e4fb))
* **Google Sheets Node:** Append fails if cells have some default values added by data validation rules ([#9950](https://github.com/n8n-io/n8n/issues/9950)) ([d1821eb](https://github.com/n8n-io/n8n/commit/d1821eba9221eb243b62ad561193102b24dd05a5))
* **Invoice Ninja Node:** Fix assigning an invoice to a payment ([#9590](https://github.com/n8n-io/n8n/issues/9590)) ([7a3c127](https://github.com/n8n-io/n8n/commit/7a3c127b2cbea01f9a21c8d517d1dc919bc8121f))
* **Invoice Ninja Node:** Fix emailing and marking invoice as paid / sent ([#9589](https://github.com/n8n-io/n8n/issues/9589)) ([908ddd8](https://github.com/n8n-io/n8n/commit/908ddd8a24e8a858d9c1eddf2f727234e66a62f7))
### Features
* **Chat Trigger Node:** Add support for file uploads & harmonize public and development chat ([#9802](https://github.com/n8n-io/n8n/issues/9802)) ([df78315](https://github.com/n8n-io/n8n/commit/df783151b86e2db3e325d3b9d85f4abb71d3d246))
* **Google Cloud Firestore Node:** Add support for service account and document creation with id ([#9713](https://github.com/n8n-io/n8n/issues/9713)) ([cb1bbf5](https://github.com/n8n-io/n8n/commit/cb1bbf5fd395ec4855ac21d851b180c8526b698a))
* **Orbit Node:** Deprecate Orbit nicely ([#9962](https://github.com/n8n-io/n8n/issues/9962)) ([9577d9c](https://github.com/n8n-io/n8n/commit/9577d9c847b56d9907d2bbe9ec85127bb8f67cfa))
* Qdrant Vector Store search filter ([#9900](https://github.com/n8n-io/n8n/issues/9900)) ([fbe4bca](https://github.com/n8n-io/n8n/commit/fbe4bca634e8e03c9455843e1a1f89706d1557d2))
* **Splunk Node:** Overhaul ([#9813](https://github.com/n8n-io/n8n/issues/9813)) ([e5c3247](https://github.com/n8n-io/n8n/commit/e5c324753fb41752f9722d61c5d336d6e5c67cca))
* **Telegram Node:** Add support to Keyboard Button Mini Apps ([#9511](https://github.com/n8n-io/n8n/issues/9511)) ([3a17943](https://github.com/n8n-io/n8n/commit/3a179439c7586189b8264131fd16da9d14f074b6))
# [1.49.0](https://github.com/n8n-io/n8n/compare/n8n@1.48.0...n8n@1.49.0) (2024-07-03)
### Bug Fixes
* **core:** Add a WebCrypto Polyfill for older versions of Node.js 18 ([#9894](https://github.com/n8n-io/n8n/issues/9894)) ([59c8bf1](https://github.com/n8n-io/n8n/commit/59c8bf1c44057b3f798645a22ad16362401ebeed))
* **core:** Don't allow using credentials that are not part of the same project ([#9916](https://github.com/n8n-io/n8n/issues/9916)) ([ab2a548](https://github.com/n8n-io/n8n/commit/ab2a5488560a814fc72c0c5cd71e5f62f05cd235))
* **core:** Filter out certain executions from crash recovery ([#9904](https://github.com/n8n-io/n8n/issues/9904)) ([7044d1c](https://github.com/n8n-io/n8n/commit/7044d1ca2841b6d87ae929072bb94dda82909795))
* **core:** Fix AddActivatedAtUserSetting migration on MariaDB ([#9910](https://github.com/n8n-io/n8n/issues/9910)) ([db29e84](https://github.com/n8n-io/n8n/commit/db29e84666b814fd4710dc3ade6e53304216fad5))
* **core:** Fix execution cancellation in scaling mode ([#9841](https://github.com/n8n-io/n8n/issues/9841)) ([e613de2](https://github.com/n8n-io/n8n/commit/e613de28ca2db23746b586e0a0b33f1c1ee1abe5))
* **core:** Fix worker logs relay ([#9919](https://github.com/n8n-io/n8n/issues/9919)) ([7c53433](https://github.com/n8n-io/n8n/commit/7c5343319144ce3524b14018eef77eace221b608))
* **core:** Throw on adding execution without execution data ([#9903](https://github.com/n8n-io/n8n/issues/9903)) ([abb7458](https://github.com/n8n-io/n8n/commit/abb74587db88a56453b269826885df0d01766290))
* **editor:** Don't try to load credentials on the demo route ([#9926](https://github.com/n8n-io/n8n/issues/9926)) ([b80df2a](https://github.com/n8n-io/n8n/commit/b80df2a47ebe4450862e200c9cf47f6e94012c91))
* **editor:** Enable expression preview in SQL node when looking at executions ([#9733](https://github.com/n8n-io/n8n/issues/9733)) ([d9747d5](https://github.com/n8n-io/n8n/commit/d9747d5e9b42d7f379f6f4219b960893b7b153b3))
* **editor:** Fix frontend project roles ([#9901](https://github.com/n8n-io/n8n/issues/9901)) ([f229577](https://github.com/n8n-io/n8n/commit/f2295772094ff936e210f52ebcbc938915d1c129))
* **editor:** Fix new node credential creation via Resource Locator Component ([#9896](https://github.com/n8n-io/n8n/issues/9896)) ([55cbc90](https://github.com/n8n-io/n8n/commit/55cbc900a48c579b712dddfa74e133e1d9c11799))
* **editor:** Fix performance issues related to expressions and pinned data ([#9882](https://github.com/n8n-io/n8n/issues/9882)) ([13d83f2](https://github.com/n8n-io/n8n/commit/13d83f2037d659fccc8889dd994ddd984467d987))
* **editor:** Improve text wrapping in schema view ([#9888](https://github.com/n8n-io/n8n/issues/9888)) ([dc1c5fc](https://github.com/n8n-io/n8n/commit/dc1c5fce8af732c438d2f1698ee08f18d2358a6c))
* **Execute Workflow Node:** Continue on fail behaviour not correctly implemented ([#9890](https://github.com/n8n-io/n8n/issues/9890)) ([16b1a09](https://github.com/n8n-io/n8n/commit/16b1a094b19e5f803a460b99c6062a1175bec153))
* **LinkedIn Node:** Fix issue with legacy credential no longer working ([#9912](https://github.com/n8n-io/n8n/issues/9912)) ([873b7e5](https://github.com/n8n-io/n8n/commit/873b7e59dcea276c9f792570805a6de8ad4607a3))
### Features
* Add Zep Cloud Memory component ([#9657](https://github.com/n8n-io/n8n/issues/9657)) ([41c47a2](https://github.com/n8n-io/n8n/commit/41c47a28a9d4502287ca1bbbb4704f2763288a11))
* **Copper Node:** Update credential to support HTTP Request node ([#9837](https://github.com/n8n-io/n8n/issues/9837)) ([e6ad5a7](https://github.com/n8n-io/n8n/commit/e6ad5a71935a5f82168bf300246ccb3535648b0b))
* **editor:** Add docs sidebar to credential modal ([#9914](https://github.com/n8n-io/n8n/issues/9914)) ([b2f8ea7](https://github.com/n8n-io/n8n/commit/b2f8ea7918d7e10e91db0e04ef5b7ad40a5bdbb5))
* **editor:** Remove Segment ([#9878](https://github.com/n8n-io/n8n/issues/9878)) ([10f7d4b](https://github.com/n8n-io/n8n/commit/10f7d4b5b92013407c9a4eb9edd619d385efe10f))
* **Embeddings Cohere Node:** Add v3 Cohere models ([#9887](https://github.com/n8n-io/n8n/issues/9887)) ([403e19b](https://github.com/n8n-io/n8n/commit/403e19b3e316db81b62eb456b38e7325bf13529c))
* **GitHub Node:** Add support for state reasons when editing an issue ([#9848](https://github.com/n8n-io/n8n/issues/9848)) ([61c20d1](https://github.com/n8n-io/n8n/commit/61c20d1ae3c65b04c767c5b704c4fc4efd356ccf))
* Introduce debug info button ([#9895](https://github.com/n8n-io/n8n/issues/9895)) ([be9a247](https://github.com/n8n-io/n8n/commit/be9a247577ffc28559a23fea2db9b2c598dca036))
* **Merge Node:** Overhaul, v3 ([#9528](https://github.com/n8n-io/n8n/issues/9528)) ([af69c80](https://github.com/n8n-io/n8n/commit/af69c80bf5a22f80979405041210dc77d2682c51))
* **Vector Store Tool Node:** Add Vector Store Tool ([#9865](https://github.com/n8n-io/n8n/issues/9865)) ([df2bc84](https://github.com/n8n-io/n8n/commit/df2bc84d2b3830d31319c108f1b01256de95e774))
* **Zammad Node:** Add reply_to and sender fields to article on ticket creation ([#9911](https://github.com/n8n-io/n8n/issues/9911)) ([957b2d6](https://github.com/n8n-io/n8n/commit/957b2d6108dccd9495291c4764816cc27e112e87))
# [1.48.0](https://github.com/n8n-io/n8n/compare/n8n@1.47.0...n8n@1.48.0) (2024-06-27) # [1.48.0](https://github.com/n8n-io/n8n/compare/n8n@1.47.0...n8n@1.48.0) (2024-06-27)

View file

@ -95,8 +95,8 @@ development environment ready in minutes.
## License ## License
n8n is [fair-code](https://faircode.io) distributed under the n8n is [fair-code](https://faircode.io) distributed under the
[**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) and the [**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/LICENSE.md) and the
[**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE_EE.md). [**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/LICENSE_EE.md).
Proprietary licenses are available for enterprise customers. [Get in touch](mailto:license@n8n.io) Proprietary licenses are available for enterprise customers. [Get in touch](mailto:license@n8n.io)

51
biome.jsonc Normal file
View file

@ -0,0 +1,51 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"vcs": {
"clientKind": "git",
"enabled": true,
"useIgnoreFile": true
},
"files": {
"ignore": [
"**/.turbo",
"**/coverage",
"**/dist",
"**/package.json",
"**/pnpm-lock.yaml",
"**/CHANGELOG.md"
]
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "tab",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100,
"attributePosition": "auto",
"ignore": [
// Handled by prettier
"**/*.vue"
]
},
"organizeImports": { "enabled": false },
"linter": {
"enabled": false
},
"javascript": {
"parser": {
"unsafeParameterDecoratorsEnabled": true
},
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "all",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSpacing": true,
"bracketSameLine": false,
"quoteStyle": "single",
"attributePosition": "auto"
}
}
}

7
cypress/biome.jsonc Normal file
View file

@ -0,0 +1,7 @@
{
"$schema": "../node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["../biome.jsonc"],
"formatter": {
"ignore": ["fixtures/**"]
}
}

View file

@ -7,15 +7,15 @@ export function getManualChatModal() {
} }
export function getManualChatInput() { export function getManualChatInput() {
return cy.getByTestId('workflow-chat-input'); return getManualChatModal().get('.chat-inputs textarea');
} }
export function getManualChatSendButton() { export function getManualChatSendButton() {
return getManualChatModal().getByTestId('workflow-chat-send-button'); return getManualChatModal().get('.chat-input-send-button');
} }
export function getManualChatMessages() { export function getManualChatMessages() {
return getManualChatModal().get('.messages .message'); return getManualChatModal().get('.chat-messages-list .chat-message');
} }
export function getManualChatModalCloseButton() { export function getManualChatModalCloseButton() {

View file

@ -0,0 +1,3 @@
export function getSaveChangesModal() {
return cy.get('.el-overlay').contains('Save changes before leaving?');
}

View file

@ -59,7 +59,7 @@ export function setCredentialByName(name: string) {
export function clickCreateNewCredential() { export function clickCreateNewCredential() {
openCredentialSelect(); openCredentialSelect();
getCreateNewCredentialOption().click(); getCreateNewCredentialOption().click({ force: true });
} }
export function clickGetBackToCanvas() { export function clickGetBackToCanvas() {

View file

@ -1,41 +1,43 @@
import { CredentialsModal, WorkflowPage } from '../pages'; import { CredentialsModal, WorkflowPage } from '../pages';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const credentialsModal = new CredentialsModal(); const credentialsModal = new CredentialsModal();
export const getHomeButton = () => cy.getByTestId('project-home-menu-item'); export const getHomeButton = () => cy.getByTestId('project-home-menu-item');
export const getMenuItems = () => cy.getByTestId('project-menu-item'); export const getMenuItems = () => cy.getByTestId('project-menu-item');
export const getAddProjectButton = () => cy.getByTestId('add-project-menu-item'); export const getAddProjectButton = () =>
cy.getByTestId('add-project-menu-item').should('contain', 'Add project').should('be.visible');
export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a'); export const getProjectTabs = () => cy.getByTestId('project-tabs').find('a');
export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]'); export const getProjectTabWorkflows = () => getProjectTabs().filter('a[href$="/workflows"]');
export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]'); export const getProjectTabCredentials = () => getProjectTabs().filter('a[href$="/credentials"]');
export const getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]'); export const getProjectTabSettings = () => getProjectTabs().filter('a[href$="/settings"]');
export const getProjectSettingsNameInput = () => cy.getByTestId('project-settings-name-input'); export const getProjectSettingsNameInput = () =>
cy.getByTestId('project-settings-name-input').find('input');
export const getProjectSettingsSaveButton = () => cy.getByTestId('project-settings-save-button'); export const getProjectSettingsSaveButton = () => cy.getByTestId('project-settings-save-button');
export const getProjectSettingsCancelButton = () => export const getProjectSettingsCancelButton = () =>
cy.getByTestId('project-settings-cancel-button'); cy.getByTestId('project-settings-cancel-button');
export const getProjectSettingsDeleteButton = () => export const getProjectSettingsDeleteButton = () =>
cy.getByTestId('project-settings-delete-button'); cy.getByTestId('project-settings-delete-button');
export const getProjectMembersSelect = () => cy.getByTestId('project-members-select'); export const getProjectMembersSelect = () => cy.getByTestId('project-members-select');
export const addProjectMember = (email: string) => { export const addProjectMember = (email: string, role?: string) => {
getProjectMembersSelect().click(); getProjectMembersSelect().click();
getProjectMembersSelect().get('.el-select-dropdown__item').contains(email.toLowerCase()).click(); getProjectMembersSelect().get('.el-select-dropdown__item').contains(email.toLowerCase()).click();
if (role) {
cy.getByTestId(`user-list-item-${email}`)
.find('[data-test-id="projects-settings-user-role-select"]')
.click();
getVisibleSelect().find('li').contains(role).click();
}
}; };
export const getProjectNameInput = () => cy.get('#projectName');
export const getResourceMoveModal = () => cy.getByTestId('project-move-resource-modal'); export const getResourceMoveModal = () => cy.getByTestId('project-move-resource-modal');
export const getResourceMoveConfirmModal = () =>
cy.getByTestId('project-move-resource-confirm-modal');
export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select'); export const getProjectMoveSelect = () => cy.getByTestId('project-move-resource-modal-select');
export function createProject(name: string) { export function createProject(name: string) {
getAddProjectButton().should('be.visible').click(); getAddProjectButton().click();
getProjectNameInput() getProjectSettingsNameInput().should('be.visible').clear().type(name);
.should('be.visible')
.should('be.focused')
.should('have.value', 'My project')
.clear()
.type(name);
getProjectSettingsSaveButton().click(); getProjectSettingsSaveButton().click();
} }
@ -46,7 +48,7 @@ export function createWorkflow(fixtureKey: string, name: string) {
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
} }
export function createCredential(name: string) { export function createCredential(name: string, closeModal = true) {
credentialsModal.getters.newCredentialModal().should('be.visible'); credentialsModal.getters.newCredentialModal().should('be.visible');
credentialsModal.getters.newCredentialTypeSelect().should('be.visible'); credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').click(); credentialsModal.getters.newCredentialTypeOption('Notion API').click();
@ -54,13 +56,8 @@ export function createCredential(name: string) {
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890'); credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
credentialsModal.actions.setName(name); credentialsModal.actions.setName(name);
credentialsModal.actions.save(); credentialsModal.actions.save();
credentialsModal.actions.close();
}
export const actions = { if (closeModal) {
createProject: (name: string) => { credentialsModal.actions.close();
getAddProjectButton().click(); }
getProjectSettingsNameInput().type(name); }
getProjectSettingsSaveButton().click();
},
};

View file

@ -1,5 +1,5 @@
import { ROUTES } from '../constants';
import { getManualChatModal } from './modals/chat-modal'; import { getManualChatModal } from './modals/chat-modal';
import { ROUTES } from '../constants';
/** /**
* Types * Types
@ -144,6 +144,12 @@ export function addToolNodeToParent(nodeName: string, parentNodeName: string) {
export function addOutputParserNodeToParent(nodeName: string, parentNodeName: string) { export function addOutputParserNodeToParent(nodeName: string, parentNodeName: string) {
addSupplementalNodeToParent(nodeName, 'ai_outputParser', parentNodeName); addSupplementalNodeToParent(nodeName, 'ai_outputParser', parentNodeName);
} }
export function addVectorStoreNodeToParent(nodeName: string, parentNodeName: string) {
addSupplementalNodeToParent(nodeName, 'ai_vectorStore', parentNodeName);
}
export function addRetrieverNodeToParent(nodeName: string, parentNodeName: string) {
addSupplementalNodeToParent(nodeName, 'ai_retriever', parentNodeName);
}
export function clickExecuteWorkflowButton() { export function clickExecuteWorkflowButton() {
getExecuteWorkflowButton().click(); getExecuteWorkflowButton().click();

View file

@ -37,6 +37,7 @@ export const INSTANCE_MEMBERS = [
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking Test workflow'; export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking Test workflow';
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger'; export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
export const CHAT_TRIGGER_NODE_DISPLAY_NAME = 'When chat message received';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code'; export const CODE_NODE_NAME = 'Code';
export const SET_NODE_NAME = 'Set'; export const SET_NODE_NAME = 'Set';
@ -53,11 +54,14 @@ export const AGENT_NODE_NAME = 'AI Agent';
export const BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain'; export const BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain';
export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Window Buffer Memory'; export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Window Buffer Memory';
export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator'; export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator';
export const AI_TOOL_CODE_NODE_NAME = 'Custom Code Tool'; export const AI_TOOL_CODE_NODE_NAME = 'Code Tool';
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia'; export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
export const AI_TOOL_HTTP_NODE_NAME = 'HTTP Request Tool';
export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model'; export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model';
export const AI_MEMORY_POSTGRES_NODE_NAME = 'Postgres Chat Memory';
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser'; export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
export const WEBHOOK_NODE_NAME = 'Webhook'; export const WEBHOOK_NODE_NAME = 'Webhook';
export const EXECUTE_WORKFLOW_NODE_NAME = 'Execute Workflow';
export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl'; export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl';

View file

@ -25,9 +25,4 @@ module.exports = defineConfig({
screenshotsFolder: 'screenshots', screenshotsFolder: 'screenshots',
videosFolder: 'videos', videosFolder: 'videos',
}, },
env: {
MAX_PINNED_DATA_SIZE: process.env.VUE_APP_MAX_PINNED_DATA_SIZE
? parseInt(process.env.VUE_APP_MAX_PINNED_DATA_SIZE, 10)
: 16 * 1024,
},
}); });

View file

@ -1,5 +1,5 @@
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { getUniqueWorkflowName } from '../utils/workflowUtils'; import { getUniqueWorkflowName } from '../utils/workflowUtils';
const WorkflowsPage = new WorkflowsPageClass(); const WorkflowsPage = new WorkflowsPageClass();
@ -73,4 +73,28 @@ describe('Workflows', () => {
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible'); WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
}); });
it('should respect tag querystring filter when listing workflows', () => {
WorkflowsPage.getters.newWorkflowButtonCard().click();
cy.createFixtureWorkflow('Test_workflow_2.json', getUniqueWorkflowName('My New Workflow'));
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.createWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.workflowFilterButton().click();
WorkflowsPage.getters.workflowTagsDropdown().click();
WorkflowsPage.getters.workflowTagItem('some-tag-1').click();
cy.reload();
WorkflowsPage.getters.workflowCards().should('have.length', 1);
});
}); });

View file

@ -1,6 +1,6 @@
import { SettingsLogStreamingPage } from '../pages'; import { SettingsLogStreamingPage } from '../pages';
import { getVisibleModalOverlay } from '../utils/modal';
import { getVisibleDropdown } from '../utils'; import { getVisibleDropdown } from '../utils';
import { getVisibleModalOverlay } from '../utils/modal';
const settingsLogStreamingPage = new SettingsLogStreamingPage(); const settingsLogStreamingPage = new SettingsLogStreamingPage();

View file

@ -4,9 +4,9 @@ import {
SET_NODE_NAME, SET_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME,
} from '../constants'; } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { MessageBox as MessageBoxClass } from '../pages/modals/message-box'; import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
import { NDV } from '../pages/ndv'; import { NDV } from '../pages/ndv';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
// Suite-specific constants // Suite-specific constants
const CODE_NODE_NEW_NAME = 'Something else'; const CODE_NODE_NEW_NAME = 'Something else';
@ -20,24 +20,7 @@ describe('Undo/Redo', () => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
}); });
it('should undo/redo adding nodes', () => { // FIXME: Canvas V2: Fix redo connections
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 0);
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
});
it('should undo/redo adding connected nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
});
it('should undo/redo adding node in the middle', () => { it('should undo/redo adding node in the middle', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -132,6 +115,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
}); });
// FIXME: Canvas V2: Fix moving of nodes via e2e tests
it('should undo/redo moving nodes', () => { it('should undo/redo moving nodes', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -164,18 +148,14 @@ describe('Undo/Redo', () => {
it('should undo/redo deleting a connection using context menu', () => { it('should undo/redo deleting a connection using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().realHover(); WorkflowPage.actions.deleteNodeBetweenNodes(SCHEDULE_TRIGGER_NODE_NAME, CODE_NODE_NAME);
cy.get('.connection-actions .delete')
.filter(':visible')
.should('be.visible')
.click({ force: true });
WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.actions.hitRedo(); WorkflowPage.actions.hitRedo();
WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
}); });
// FIXME: Canvas V2: Fix disconnecting by moving
it('should undo/redo deleting a connection by moving it away', () => { it('should undo/redo deleting a connection by moving it away', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -224,21 +204,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.disabledNodes().should('have.length', 2); WorkflowPage.getters.disabledNodes().should('have.length', 2);
}); });
it('should undo/redo renaming node using NDV', () => { // FIXME: Canvas V2: Fix undo renaming node
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
ndv.actions.rename(CODE_NODE_NEW_NAME);
cy.get('body').type('{esc}');
WorkflowPage.actions.hitUndo();
cy.get('body').type('{esc}');
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).should('exist');
WorkflowPage.actions.hitRedo();
cy.get('body').type('{esc}');
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NEW_NAME).should('exist');
});
it('should undo/redo renaming node using keyboard shortcut', () => { it('should undo/redo renaming node using keyboard shortcut', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
@ -277,6 +243,7 @@ describe('Undo/Redo', () => {
}); });
}); });
// FIXME: Canvas V2: Figure out why moving doesn't work from e2e
it('should undo/redo multiple steps', () => { it('should undo/redo multiple steps', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);

View file

@ -11,6 +11,21 @@ describe('Inline expression editor', () => {
cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError'); cy.on('uncaught:exception', (error) => error.name !== 'ExpressionError');
}); });
describe('Basic UI functionality', () => {
it('should open and close inline expression preview', () => {
WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.openNode('Schedule');
WorkflowPage.actions.openInlineExpressionEditor();
WorkflowPage.getters.inlineExpressionEditorInput().clear();
WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{');
WorkflowPage.getters.inlineExpressionEditorInput().type('123');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^123$/);
// click outside to close
ndv.getters.outputPanel().click();
WorkflowPage.getters.inlineExpressionEditorOutput().should('not.exist');
});
});
describe('Static data', () => { describe('Static data', () => {
beforeEach(() => { beforeEach(() => {
WorkflowPage.actions.addNodeToCanvas('Hacker News'); WorkflowPage.actions.addNodeToCanvas('Hacker News');

View file

@ -1,5 +1,3 @@
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { successToast } from '../pages/notifications';
import { import {
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME,
@ -9,6 +7,8 @@ import {
IF_NODE_NAME, IF_NODE_NAME,
HTTP_REQUEST_NODE_NAME, HTTP_REQUEST_NODE_NAME,
} from './../constants'; } from './../constants';
import { successToast } from '../pages/notifications';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
describe('Canvas Actions', () => { describe('Canvas Actions', () => {
@ -16,6 +16,7 @@ describe('Canvas Actions', () => {
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
}); });
// FIXME: Canvas V2: Missing execute button if no nodes
it('should render canvas', () => { it('should render canvas', () => {
WorkflowPage.getters.nodeViewRoot().should('be.visible'); WorkflowPage.getters.nodeViewRoot().should('be.visible');
WorkflowPage.getters.canvasPlusButton().should('be.visible'); WorkflowPage.getters.canvasPlusButton().should('be.visible');
@ -25,10 +26,11 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.executeWorkflowButton().should('be.visible'); WorkflowPage.getters.executeWorkflowButton().should('be.visible');
}); });
// FIXME: Canvas V2: Fix changing of connection
it('should connect and disconnect a simple node', () => { it('should connect and disconnect a simple node', () => {
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true }); WorkflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
cy.get('.jtk-connector').should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.getters.nodeViewBackground().click(600, 400, { force: true }); WorkflowPage.getters.nodeViewBackground().click(600, 400, { force: true });
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
@ -40,16 +42,16 @@ describe('Canvas Actions', () => {
); );
WorkflowPage.getters WorkflowPage.getters
.canvasNodeInputEndpointByName(`${EDIT_FIELDS_SET_NODE_NAME}1`) .getConnectionBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, `${EDIT_FIELDS_SET_NODE_NAME}1`)
.should('have.class', 'jtk-endpoint-connected'); .should('be.visible');
cy.get('.jtk-connector').should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
// Disconnect Set1 // Disconnect Set1
cy.drag( cy.drag(
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`), WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
[-200, 100], [-200, 100],
); );
cy.get('.jtk-connector').should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
}); });
it('should add first step', () => { it('should add first step', () => {
@ -74,7 +76,7 @@ describe('Canvas Actions', () => {
it('should add a connected node using plus endpoint', () => { it('should add a connected node using plus endpoint', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
cy.get('.plus-endpoint').should('be.visible').click(); WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME); WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}'); WorkflowPage.getters.nodeCreatorSearchBar().type('{enter}');
@ -85,7 +87,7 @@ describe('Canvas Actions', () => {
it('should add a connected node dragging from node creator', () => { it('should add a connected node dragging from node creator', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
cy.get('.plus-endpoint').should('be.visible').click(); WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME); WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
cy.drag(WorkflowPage.getters.nodeCreatorNodeItems().first(), [100, 100], { cy.drag(WorkflowPage.getters.nodeCreatorNodeItems().first(), [100, 100], {
@ -99,7 +101,7 @@ describe('Canvas Actions', () => {
it('should open a category when trying to drag and drop it on the canvas', () => { it('should open a category when trying to drag and drop it on the canvas', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
cy.get('.plus-endpoint').should('be.visible').click(); WorkflowPage.getters.canvasNodePlusEndpointByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME); WorkflowPage.getters.nodeCreatorSearchBar().type(CODE_NODE_NAME);
cy.drag(WorkflowPage.getters.nodeCreatorActionItems().first(), [100, 100], { cy.drag(WorkflowPage.getters.nodeCreatorActionItems().first(), [100, 100], {
@ -114,7 +116,7 @@ describe('Canvas Actions', () => {
it('should add disconnected node if nothing is selected', () => { it('should add disconnected node if nothing is selected', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
// Deselect nodes // Deselect nodes
WorkflowPage.getters.nodeViewBackground().click({ force: true }); WorkflowPage.getters.nodeView().click({ force: true });
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
@ -136,24 +138,35 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 3); WorkflowPage.getters.nodeConnections().should('have.length', 3);
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => { WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).then(($editFieldsNode) => {
const editFieldsNodeLeft = parseFloat($editFieldsNode.css('left')); const editFieldsNodeLeft = WorkflowPage.getters.getNodeLeftPosition($editFieldsNode);
WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => { WorkflowPage.getters.canvasNodeByName(HTTP_REQUEST_NODE_NAME).then(($httpNode) => {
const httpNodeLeft = parseFloat($httpNode.css('left')); const httpNodeLeft = WorkflowPage.getters.getNodeLeftPosition($httpNode);
expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft); expect(httpNodeLeft).to.be.lessThan(editFieldsNodeLeft);
}); });
}); });
}); });
it('should delete connections by pressing the delete button', () => { it('should delete node by pressing keyboard backspace', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(CODE_NODE_NAME).click();
cy.get('body').type('{backspace}');
WorkflowPage.getters.nodeConnections().should('have.length', 0);
});
it('should delete connections by clicking on the delete button', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().first().realHover(); WorkflowPage.getters.nodeConnections().first().realHover();
cy.get('.connection-actions .delete').first().click({ force: true }); WorkflowPage.actions.deleteNodeBetweenNodes(MANUAL_TRIGGER_NODE_DISPLAY_NAME, CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
}); });
// FIXME: Canvas V2: Fix disconnecting of connection by dragging it
it('should delete a connection by moving it away from endpoint', () => { it('should delete a connection by moving it away from endpoint', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
@ -207,10 +220,10 @@ describe('Canvas Actions', () => {
WorkflowPage.actions.hitSelectAll(); WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitCopy(); WorkflowPage.actions.hitCopy();
successToast().should('contain', 'Copied!'); successToast().should('contain', 'Copied to clipboard');
WorkflowPage.actions.copyNode(CODE_NODE_NAME); WorkflowPage.actions.copyNode(CODE_NODE_NAME);
successToast().should('contain', 'Copied!'); successToast().should('contain', 'Copied to clipboard');
}); });
it('should select/deselect all nodes', () => { it('should select/deselect all nodes', () => {
@ -222,17 +235,31 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.selectedNodes().should('have.length', 0); WorkflowPage.getters.selectedNodes().should('have.length', 0);
}); });
// FIXME: Canvas V2: Selection via arrow keys is broken
it('should select nodes using arrow keys', () => { it('should select nodes using arrow keys', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500); cy.wait(500);
cy.get('body').type('{leftArrow}'); cy.get('body').type('{leftArrow}');
WorkflowPage.getters.canvasNodes().first().should('have.class', 'jtk-drag-selected'); const selectedCanvasNodes = () =>
cy.ifCanvasVersion(
() => WorkflowPage.getters.canvasNodes(),
() => WorkflowPage.getters.canvasNodes().parent(),
);
cy.ifCanvasVersion(
() => selectedCanvasNodes().first().should('have.class', 'jtk-drag-selected'),
() => selectedCanvasNodes().first().should('have.class', 'selected'),
);
cy.get('body').type('{rightArrow}'); cy.get('body').type('{rightArrow}');
WorkflowPage.getters.canvasNodes().last().should('have.class', 'jtk-drag-selected'); cy.ifCanvasVersion(
() => selectedCanvasNodes().last().should('have.class', 'jtk-drag-selected'),
() => selectedCanvasNodes().last().should('have.class', 'selected'),
);
}); });
// FIXME: Canvas V2: Selection via shift and arrow keys is broken
it('should select nodes using shift and arrow keys', () => { it('should select nodes using shift and arrow keys', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
@ -242,6 +269,7 @@ describe('Canvas Actions', () => {
WorkflowPage.getters.selectedNodes().should('have.length', 2); WorkflowPage.getters.selectedNodes().should('have.length', 2);
}); });
// FIXME: Canvas V2: Fix select & deselect
it('should not break lasso selection when dragging node action buttons', () => { it('should not break lasso selection when dragging node action buttons', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters WorkflowPage.getters
@ -253,6 +281,7 @@ describe('Canvas Actions', () => {
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]); WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
}); });
// FIXME: Canvas V2: Fix select & deselect
it('should not break lasso selection with multiple clicks on node action buttons', () => { it('should not break lasso selection with multiple clicks on node action buttons', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]); WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);

View file

@ -1,5 +1,3 @@
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV, WorkflowExecutionsTab } from '../pages';
import { import {
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME,
@ -9,6 +7,9 @@ import {
SWITCH_NODE_NAME, SWITCH_NODE_NAME,
MERGE_NODE_NAME, MERGE_NODE_NAME,
} from './../constants'; } from './../constants';
import { NDV, WorkflowExecutionsTab } from '../pages';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { isCanvasV2 } from '../utils/workflowUtils';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
const ExecutionsTab = new WorkflowExecutionsTab(); const ExecutionsTab = new WorkflowExecutionsTab();
@ -52,15 +53,15 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.reload(); cy.reload();
cy.waitForLoad(); cy.waitForLoad();
// Make sure outputless switch was connected correctly // Make sure outputless switch was connected correctly
cy.get( WorkflowPage.getters
`[data-target-node="${SWITCH_NODE_NAME}1"][data-source-node="${EDIT_FIELDS_SET_NODE_NAME}3"]`, .getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}3`, `${SWITCH_NODE_NAME}1`)
).should('be.visible'); .should('exist');
// Make sure all connections are there after reload // Make sure all connections are there after reload
for (let i = 0; i < desiredOutputs; i++) { for (let i = 0; i < desiredOutputs; i++) {
const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`; const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`;
WorkflowPage.getters WorkflowPage.getters
.canvasNodeInputEndpointByName(setName) .getConnectionBetweenNodes(`${SWITCH_NODE_NAME}`, setName)
.should('have.class', 'jtk-endpoint-connected'); .should('exist');
} }
}); });
@ -69,9 +70,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true); WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true);
WorkflowPage.getters WorkflowPage.getters.nodeView().click((i + 1) * 200, (i + 1) * 200, { force: true });
.nodeViewBackground()
.click((i + 1) * 200, (i + 1) * 200, { force: true });
} }
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
@ -84,8 +83,6 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`), WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
); );
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 2);
// Connect Set1 and Set2 to merge // Connect Set1 and Set2 to merge
cy.draganddrop( cy.draganddrop(
WorkflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME), WorkflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME),
@ -95,20 +92,36 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.getEndpointSelector('plus', `${EDIT_FIELDS_SET_NODE_NAME}1`), WorkflowPage.getters.getEndpointSelector('plus', `${EDIT_FIELDS_SET_NODE_NAME}1`),
WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1), WorkflowPage.getters.getEndpointSelector('input', MERGE_NODE_NAME, 1),
); );
const checkConnections = () => {
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4); WorkflowPage.getters
.getConnectionBetweenNodes(
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
`${EDIT_FIELDS_SET_NODE_NAME}1`,
)
.should('exist');
WorkflowPage.getters
.getConnectionBetweenNodes(EDIT_FIELDS_SET_NODE_NAME, MERGE_NODE_NAME)
.should('exist');
WorkflowPage.getters
.getConnectionBetweenNodes(`${EDIT_FIELDS_SET_NODE_NAME}1`, MERGE_NODE_NAME)
.should('exist');
};
checkConnections();
// Make sure all connections are there after save & reload // Make sure all connections are there after save & reload
WorkflowPage.actions.saveWorkflowOnButtonClick(); WorkflowPage.actions.saveWorkflowOnButtonClick();
cy.reload(); cy.reload();
cy.waitForLoad(); cy.waitForLoad();
checkConnections();
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4); // cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 4);
WorkflowPage.actions.executeWorkflow(); WorkflowPage.actions.executeWorkflow();
WorkflowPage.getters.stopExecutionButton().should('not.exist'); WorkflowPage.getters.stopExecutionButton().should('not.exist');
// If the merged set nodes are connected and executed correctly, there should be 2 items in the output of merge node // If the merged set nodes are connected and executed correctly, there should be 2 items in the output of merge node
cy.get('[data-label="2 items"]').should('be.visible'); cy.ifCanvasVersion(
() => cy.get('[data-label="2 items"]').should('be.visible'),
() => cy.getByTestId('canvas-node-output-handle').contains('2 items').should('be.visible'),
);
}); });
it('should add nodes and check execution success', () => { it('should add nodes and check execution success', () => {
@ -120,16 +133,42 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.executeWorkflow(); WorkflowPage.actions.executeWorkflow();
cy.get('.jtk-connector.success').should('have.length', 3); cy.ifCanvasVersion(
cy.get('.data-count').should('have.length', 4); () => cy.get('.jtk-connector.success').should('have.length', 3),
cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success'); () => cy.get('[data-edge-status=success]').should('have.length', 3),
);
cy.ifCanvasVersion(
() => cy.get('.data-count').should('have.length', 4),
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
);
cy.ifCanvasVersion(
() => cy.get('.plus-draggable-endpoint').should('have.class', 'ep-success'),
() => cy.getByTestId('canvas-handle-plus').should('have.attr', 'data-plus-type', 'success'),
);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
cy.get('.plus-draggable-endpoint').filter(':visible').should('not.have.class', 'ep-success'); cy.ifCanvasVersion(
cy.get('.jtk-connector.success').should('have.length', 3); () =>
cy.get('.jtk-connector').should('have.length', 4); cy
.get('.plus-draggable-endpoint')
.filter(':visible')
.should('not.have.class', 'ep-success'),
() =>
cy.getByTestId('canvas-handle-plus').should('not.have.attr', 'data-plus-type', 'success'),
);
cy.ifCanvasVersion(
() => cy.get('.jtk-connector.success').should('have.length', 3),
// The new version of the canvas correctly shows executed data being passed to the input of the next node
() => cy.get('[data-edge-status=success]').should('have.length', 4),
);
cy.ifCanvasVersion(
() => cy.get('.data-count').should('have.length', 4),
() => cy.getByTestId('canvas-node-status-success').should('have.length', 4),
);
}); });
it('should delete node using context menu', () => { it('should delete node using context menu', () => {
@ -194,19 +233,29 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodes().should('have.length', 0); WorkflowPage.getters.canvasNodes().should('have.length', 0);
}); });
// FIXME: Canvas V2: Figure out how to test moving of the node
it('should move node', () => { it('should move node', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.last() .last()
.then(($node) => { .then(($node) => {
const { left, top } = $node.position(); const { left, top } = $node.position();
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
clickToFinish: true, if (isCanvasV2()) {
}); cy.drag('.vue-flow__node', [300, 300], {
realMouse: true,
});
} else {
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
clickToFinish: true,
});
}
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.last() .last()
@ -218,91 +267,80 @@ describe('Canvas Node Manipulation and Navigation', () => {
}); });
}); });
it('should zoom in', () => { describe('Canvas Zoom Functionality', () => {
WorkflowPage.getters.zoomInButton().should('be.visible').click(); const getContainer = () =>
WorkflowPage.getters cy.ifCanvasVersion(
.nodeView() () => WorkflowPage.getters.nodeView(),
.should( () => WorkflowPage.getters.canvasViewport(),
'have.css',
'transform',
`matrix(${ZOOM_IN_X1_FACTOR}, 0, 0, ${ZOOM_IN_X1_FACTOR}, 0, 0)`,
); );
WorkflowPage.getters.zoomInButton().click(); const checkZoomLevel = (expectedFactor: number) => {
WorkflowPage.getters return getContainer().should(($nodeView) => {
.nodeView() const newTransform = $nodeView.css('transform');
.should( const newScale = parseFloat(newTransform.split(',')[0].slice(7));
'have.css',
'transform',
`matrix(${ZOOM_IN_X2_FACTOR}, 0, 0, ${ZOOM_IN_X2_FACTOR}, 0, 0)`,
);
});
it('should zoom out', () => { expect(newScale).to.be.closeTo(expectedFactor, 0.2);
WorkflowPage.getters.zoomOutButton().should('be.visible').click(); });
WorkflowPage.getters };
.nodeView()
.should(
'have.css',
'transform',
`matrix(${ZOOM_OUT_X1_FACTOR}, 0, 0, ${ZOOM_OUT_X1_FACTOR}, 0, 0)`,
);
WorkflowPage.getters.zoomOutButton().click();
WorkflowPage.getters
.nodeView()
.should(
'have.css',
'transform',
`matrix(${ZOOM_OUT_X2_FACTOR}, 0, 0, ${ZOOM_OUT_X2_FACTOR}, 0, 0)`,
);
});
it('should zoom using scroll or pinch gesture', () => { const zoomAndCheck = (action: 'zoomIn' | 'zoomOut', expectedFactor: number) => {
WorkflowPage.actions.pinchToZoom(1, 'zoomIn'); WorkflowPage.getters[`${action}Button`]().click();
WorkflowPage.getters checkZoomLevel(expectedFactor);
.nodeView() };
.should(
'have.css', it('should zoom in', () => {
'transform', WorkflowPage.getters.zoomInButton().should('be.visible');
`matrix(${PINCH_ZOOM_IN_FACTOR}, 0, 0, ${PINCH_ZOOM_IN_FACTOR}, 0, 0)`, getContainer().then(($nodeView) => {
const initialTransform = $nodeView.css('transform');
const initialScale =
initialTransform === 'none' ? 1 : parseFloat(initialTransform.split(',')[0].slice(7));
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X1_FACTOR);
zoomAndCheck('zoomIn', initialScale * ZOOM_IN_X2_FACTOR);
});
});
it('should zoom out', () => {
zoomAndCheck('zoomOut', ZOOM_OUT_X1_FACTOR);
zoomAndCheck('zoomOut', ZOOM_OUT_X2_FACTOR);
});
it('should zoom using scroll or pinch gesture', () => {
WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
// V2 Canvas is using the same zoom factor for both pinch and scroll
cy.ifCanvasVersion(
() => checkZoomLevel(PINCH_ZOOM_IN_FACTOR),
() => checkZoomLevel(ZOOM_IN_X1_FACTOR),
); );
WorkflowPage.actions.pinchToZoom(1, 'zoomOut'); WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
// Zoom in 1x + Zoom out 1x should reset to default (=1) checkZoomLevel(1); // Zoom in 1x + Zoom out 1x should reset to default (=1)
WorkflowPage.getters.nodeView().should('have.css', 'transform', 'matrix(1, 0, 0, 1, 0, 0)');
WorkflowPage.actions.pinchToZoom(1, 'zoomOut'); WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
WorkflowPage.getters
.nodeView() cy.ifCanvasVersion(
.should( () => checkZoomLevel(PINCH_ZOOM_OUT_FACTOR),
'have.css', () => checkZoomLevel(ZOOM_OUT_X1_FACTOR),
'transform',
`matrix(${PINCH_ZOOM_OUT_FACTOR}, 0, 0, ${PINCH_ZOOM_OUT_FACTOR}, 0, 0)`,
); );
}); });
it('should reset zoom', () => { it('should reset zoom', () => {
// Reset zoom should not appear until zoom level changed WorkflowPage.getters.resetZoomButton().should('not.exist');
WorkflowPage.getters.resetZoomButton().should('not.exist'); WorkflowPage.getters.zoomInButton().click();
WorkflowPage.getters.zoomInButton().click(); WorkflowPage.getters.resetZoomButton().should('be.visible').click();
WorkflowPage.getters.resetZoomButton().should('be.visible').click(); checkZoomLevel(DEFAULT_ZOOM_FACTOR);
WorkflowPage.getters });
.nodeView()
.should(
'have.css',
'transform',
`matrix(${DEFAULT_ZOOM_FACTOR}, 0, 0, ${DEFAULT_ZOOM_FACTOR}, 0, 0)`,
);
});
it('should zoom to fit', () => { it('should zoom to fit', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
// At this point last added node should be off-screen // At this point last added node should be off-screen
WorkflowPage.getters.canvasNodes().last().should('not.be.visible'); WorkflowPage.getters.canvasNodes().last().should('not.be.visible');
WorkflowPage.getters.zoomToFitButton().click(); WorkflowPage.getters.zoomToFitButton().click();
WorkflowPage.getters.canvasNodes().last().should('be.visible'); WorkflowPage.getters.canvasNodes().last().should('be.visible');
});
}); });
it('should disable node (context menu or shortcut)', () => { it('should disable node (context menu or shortcut)', () => {
@ -426,9 +464,9 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.reload(); cy.reload();
cy.waitForLoad(); cy.waitForLoad();
WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.canvasNodes().should('have.length', 2);
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
}); });
// FIXME: Canvas V2: Credentials should show issue on the first open
it('should remove unknown credentials on pasting workflow', () => { it('should remove unknown credentials on pasting workflow', () => {
cy.fixture('workflow-with-unknown-credentials.json').then((data) => { cy.fixture('workflow-with-unknown-credentials.json').then((data) => {
cy.get('body').paste(JSON.stringify(data)); cy.get('body').paste(JSON.stringify(data));
@ -441,6 +479,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
}); });
}); });
// FIXME: Canvas V2: Unknown nodes should still render connection endpoints
it('should render connections correctly if unkown nodes are present', () => { it('should render connections correctly if unkown nodes are present', () => {
const unknownNodeName = 'Unknown node'; const unknownNodeName = 'Unknown node';
cy.createFixtureWorkflow('workflow-with-unknown-nodes.json', 'Unknown nodes'); cy.createFixtureWorkflow('workflow-with-unknown-nodes.json', 'Unknown nodes');

View file

@ -1,3 +1,6 @@
import { nanoid } from 'nanoid';
import { simpleWebhookCall, waitForWebhook } from './16-webhook-node.cy';
import { import {
HTTP_REQUEST_NODE_NAME, HTTP_REQUEST_NODE_NAME,
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
@ -7,13 +10,19 @@ import {
} from '../constants'; } from '../constants';
import { WorkflowPage, NDV } from '../pages'; import { WorkflowPage, NDV } from '../pages';
import { errorToast } from '../pages/notifications'; import { errorToast } from '../pages/notifications';
import { getVisiblePopper } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
describe('Data pinning', () => { describe('Data pinning', () => {
const maxPinnedDataSize = 16384;
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.window().then((win) => {
win.maxPinnedDataSize = maxPinnedDataSize;
});
}); });
it('Should be able to pin node output', () => { it('Should be able to pin node output', () => {
@ -139,7 +148,7 @@ describe('Data pinning', () => {
ndv.actions.pastePinnedData([ ndv.actions.pastePinnedData([
{ {
test: '1'.repeat(Cypress.env('MAX_PINNED_DATA_SIZE') as number), test: '1'.repeat(maxPinnedDataSize),
}, },
]); ]);
errorToast().should('contain', 'Workflow has reached the maximum allowed pinned data size'); errorToast().should('contain', 'Workflow has reached the maximum allowed pinned data size');
@ -207,6 +216,42 @@ describe('Data pinning', () => {
}, },
); );
}); });
it('should show pinned data tooltip', () => {
const { callEndpoint } = simpleWebhookCall({
method: 'GET',
webhookPath: nanoid(),
executeNow: false,
});
ndv.actions.close();
workflowPage.actions.executeWorkflow();
cy.wait(waitForWebhook);
// hide other visible popper on workflow execute button
workflowPage.getters.canvasNodes().eq(0).click();
callEndpoint((response) => {
expect(response.status).to.eq(200);
getVisiblePopper().should('have.length', 1);
getVisiblePopper()
.eq(0)
.should(
'have.text',
'You can pin this output instead of waiting for a test event. Open node to do so.',
);
});
});
it('should not show pinned data tooltip', () => {
cy.createFixtureWorkflow('Pinned_webhook_node.json', 'Test');
workflowPage.actions.executeWorkflow();
// hide other visible popper on workflow execute button
workflowPage.getters.canvasNodes().eq(0).click();
getVisiblePopper().should('have.length', 0);
});
}); });
function setExpressionOnStringValueInSet(expression: string) { function setExpressionOnStringValueInSet(expression: string) {

View file

@ -1,10 +1,10 @@
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
import { import {
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME,
SCHEDULE_TRIGGER_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME,
} from './../constants'; } from './../constants';
import { WorkflowPage, NDV } from '../pages';
import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -40,11 +40,13 @@ describe('Data mapping', () => {
ndv.actions.mapDataFromHeader(1, 'value'); ndv.actions.mapDataFromHeader(1, 'value');
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}'); ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.timestamp }}');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.getters.parameterExpressionPreview('value').should('include.text', '2024');
ndv.actions.mapDataFromHeader(2, 'value'); ndv.actions.mapDataFromHeader(2, 'value');
ndv.getters ndv.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.should('have.text', "{{ $json.timestamp }} {{ $json['Readable date'] }}"); .should('have.text', "{{ $json['Readable date'] }}{{ $json.timestamp }}");
}); });
it('maps expressions from table json, and resolves value based on hover', () => { it('maps expressions from table json, and resolves value based on hover', () => {
@ -133,6 +135,7 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value'); ndv.actions.mapToParameter('value');
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}'); ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.getters.parameterExpressionPreview('value').should('include.text', '0'); ndv.getters.parameterExpressionPreview('value').should('include.text', '0');
ndv.getters ndv.getters
@ -145,8 +148,9 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value'); ndv.actions.mapToParameter('value');
ndv.getters ndv.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}'); .should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
ndv.actions.validateExpressionPreview('value', '0 [object Object]'); ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.actions.validateExpressionPreview('value', '[object Object]0');
}); });
it('maps expressions from schema view', () => { it('maps expressions from schema view', () => {
@ -163,6 +167,7 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value'); ndv.actions.mapToParameter('value');
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}'); ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.actions.validateExpressionPreview('value', '0'); ndv.actions.validateExpressionPreview('value', '0');
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown(); ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
@ -170,8 +175,8 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value'); ndv.actions.mapToParameter('value');
ndv.getters ndv.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}'); .should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
ndv.actions.validateExpressionPreview('value', '0 [object Object]'); ndv.actions.validateExpressionPreview('value', '[object Object]0');
}); });
it('maps expressions from previous nodes', () => { it('maps expressions from previous nodes', () => {
@ -192,6 +197,7 @@ describe('Data mapping', () => {
ndv.getters ndv.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`); .should('have.text', `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`);
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.actions.switchInputMode('Table'); ndv.actions.switchInputMode('Table');
ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME); ndv.actions.selectInputNode(SCHEDULE_TRIGGER_NODE_NAME);
@ -200,17 +206,17 @@ describe('Data mapping', () => {
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.should( .should(
'have.text', 'have.text',
`{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }} {{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input }}`, `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input }}{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`,
); );
ndv.actions.selectInputNode('Set'); ndv.actions.selectInputNode('Set');
ndv.getters.executingLoader().should('not.exist'); ndv.getters.executingLoader().should('not.exist');
ndv.getters.inputDataContainer().should('exist'); ndv.getters.inputDataContainer().should('exist');
ndv.actions.validateExpressionPreview('value', '0 [object Object]'); ndv.actions.validateExpressionPreview('value', '[object Object]0');
ndv.getters.inputTbodyCell(2, 0).realHover(); ndv.getters.inputTbodyCell(2, 0).realHover();
ndv.actions.validateExpressionPreview('value', '1 [object Object]'); ndv.actions.validateExpressionPreview('value', '[object Object]1');
}); });
it('maps keys to path', () => { it('maps keys to path', () => {
@ -271,12 +277,12 @@ describe('Data mapping', () => {
ndv.actions.typeIntoParameterInput('value', 'fun'); ndv.actions.typeIntoParameterInput('value', 'fun');
ndv.actions.clearParameterInput('value'); // keep focus on param ndv.actions.clearParameterInput('value'); // keep focus on param
cy.wait(300);
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown(); ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();
ndv.actions.mapToParameter('value'); ndv.actions.mapToParameter('value');
ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}'); ndv.getters.inlineExpressionEditorInput().should('have.text', '{{ $json.input[0].count }}');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.actions.validateExpressionPreview('value', '0'); ndv.actions.validateExpressionPreview('value', '0');
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown(); ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
@ -284,8 +290,8 @@ describe('Data mapping', () => {
ndv.actions.mapToParameter('value'); ndv.actions.mapToParameter('value');
ndv.getters ndv.getters
.inlineExpressionEditorInput() .inlineExpressionEditorInput()
.should('have.text', '{{ $json.input[0].count }} {{ $json.input }}'); .should('have.text', '{{ $json.input }}{{ $json.input[0].count }}');
ndv.actions.validateExpressionPreview('value', '0 [object Object]'); ndv.actions.validateExpressionPreview('value', '[object Object]0');
}); });
it('renders expression preview when a previous node is selected', () => { it('renders expression preview when a previous node is selected', () => {
@ -317,19 +323,12 @@ describe('Data mapping', () => {
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set'); workflowPage.actions.openNode('Set');
ndv.actions.clearParameterInput('value');
cy.get('body').type('{esc}');
ndv.getters.parameterInput('includeOtherFields').find('input[type="checkbox"]').should('exist'); ndv.getters.parameterInput('includeOtherFields').find('input[type="checkbox"]').should('exist');
ndv.getters.parameterInput('includeOtherFields').find('input[type="text"]').should('not.exist'); ndv.getters.parameterInput('includeOtherFields').find('input[type="text"]').should('not.exist');
ndv.getters const pill = ndv.getters.inputDataContainer().find('span').contains('count');
.inputDataContainer() pill.should('be.visible');
.should('exist') pill.realMouseDown();
.find('span') pill.realMouseMove(100, 100);
.contains('count')
.realMouseDown()
.realMouseMove(100, 100);
cy.wait(50);
ndv.getters ndv.getters
.parameterInput('includeOtherFields') .parameterInput('includeOtherFields')
@ -340,13 +339,40 @@ describe('Data mapping', () => {
.find('input[type="text"]') .find('input[type="text"]')
.should('exist') .should('exist')
.invoke('css', 'border') .invoke('css', 'border')
.then((border) => expect(border).to.include('dashed rgb(90, 76, 194)')); .should('include', 'dashed rgb(90, 76, 194)');
ndv.getters ndv.getters
.parameterInput('value') .parameterInput('value')
.find('input[type="text"]') .find('input[type="text"]')
.should('exist') .should('exist')
.invoke('css', 'border') .invoke('css', 'border')
.then((border) => expect(border).to.include('dashed rgb(90, 76, 194)')); .should('include', 'dashed rgb(90, 76, 194)');
});
it('maps expressions to a specific location in the editor', () => {
cy.fixture('Test_workflow_3.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Set');
ndv.actions.typeIntoParameterInput('value', '=');
ndv.getters.inlineExpressionEditorInput().find('.cm-content').paste('hello world\n\nnewline');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown();
ndv.actions.mapToParameter('value');
ndv.getters
.inlineExpressionEditorInput()
.should('have.text', '{{ $json.input[0].count }}hello worldnewline');
ndv.getters.inlineExpressionEditorInput().type('{esc}');
ndv.actions.validateExpressionPreview('value', '0hello world\n\nnewline');
ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown();
ndv.actions.mapToParameter('value', 'center');
ndv.getters
.inlineExpressionEditorInput()
.should('have.text', '{{ $json.input[0].count }}hello world{{ $json.input }}newline');
}); });
}); });

View file

@ -1,9 +1,5 @@
import { WorkflowPage, WorkflowsPage, NDV } from '../pages'; import { WorkflowPage, NDV } from '../pages';
import { BACKEND_BASE_URL } from '../constants';
import { getVisibleSelect } from '../utils';
import type { ExecutionResponse } from '../types';
const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -19,53 +15,4 @@ describe('Schedule Trigger node', () => {
ndv.getters.outputPanel().contains('timestamp'); ndv.getters.outputPanel().contains('timestamp');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
}); });
it('should execute once per second when activated', () => {
workflowPage.actions.renameWorkflow('Schedule Trigger Workflow');
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.openNode('Schedule Trigger');
cy.getByTestId('parameter-input-field').click();
getVisibleSelect().find('.option-headline').contains('Seconds').click();
cy.getByTestId('parameter-input-secondsInterval').clear().type('1');
ndv.getters.backToCanvas().click();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.activateWorkflow();
workflowPage.getters.activatorSwitch().should('have.class', 'is-checked');
cy.url().then((url) => {
const workflowId = url.split('/').pop();
cy.wait(1200);
cy.request<ExecutionResponse>('GET', `${BACKEND_BASE_URL}/rest/executions`).then(
(response) => {
expect(response.status).to.eq(200);
expect(workflowId).to.not.be.undefined;
expect(response.body.data.results.length).to.be.greaterThan(0);
const matchingExecutions = response.body.data.results.filter(
(execution) => execution.workflowId === workflowId,
);
expect(matchingExecutions).to.have.length(1);
cy.wait(1200);
cy.request<ExecutionResponse>('GET', `${BACKEND_BASE_URL}/rest/executions`).then(
(response1) => {
expect(response1.status).to.eq(200);
expect(response1.body.data.results.length).to.be.greaterThan(0);
const matchingExecutions1 = response1.body.data.results.filter(
(execution: any) => execution.workflowId === workflowId,
);
expect(matchingExecutions1).to.have.length(2);
workflowPage.actions.activateWorkflow();
workflowPage.getters.activatorSwitch().should('not.have.class', 'is-checked');
cy.visit(workflowsPage.url);
workflowsPage.actions.deleteWorkFlow('Schedule Trigger Workflow');
},
);
},
);
});
});
}); });

View file

@ -16,12 +16,14 @@ describe('n8n Form Trigger', () => {
ndv.getters.parameterInput('formDescription').type('Test Form Description'); ndv.getters.parameterInput('formDescription').type('Test Form Description');
ndv.getters.parameterInput('fieldLabel').type('Test Field 1'); ndv.getters.parameterInput('fieldLabel').type('Test Field 1');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist'); workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist');
}); });
it('should fill up form fields', () => { it('should fill up form fields', () => {
workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger'); workflowPage.actions.addInitialNodeToCanvas('n8n Form Trigger', {
workflowPage.getters.canvasNodes().first().dblclick(); isTrigger: true,
action: 'On new n8n Form event',
});
ndv.getters.parameterInput('formTitle').type('Test Form'); ndv.getters.parameterInput('formTitle').type('Test Form');
ndv.getters.parameterInput('formDescription').type('Test Form Description'); ndv.getters.parameterInput('formDescription').type('Test Form Description');
//fill up first field of type number //fill up first field of type number
@ -96,6 +98,6 @@ describe('n8n Form Trigger', () => {
.type('Your test form was successfully submitted'); .type('Your test form was successfully submitted');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist'); workflowPage.getters.nodeIssuesByName('On form submission').should('not.exist');
}); });
}); });

View file

@ -1,14 +1,15 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { WorkflowPage, NDV, CredentialsModal } from '../pages'; import { WorkflowPage, NDV, CredentialsModal } from '../pages';
import { cowBase64 } from '../support/binaryTestFiles'; import { cowBase64 } from '../support/binaryTestFiles';
import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
const credentialsModal = new CredentialsModal(); const credentialsModal = new CredentialsModal();
const waitForWebhook = 500; export const waitForWebhook = 500;
interface SimpleWebhookCallOptions { interface SimpleWebhookCallOptions {
method: string; method: string;
@ -20,7 +21,7 @@ interface SimpleWebhookCallOptions {
authentication?: string; authentication?: string;
} }
const simpleWebhookCall = (options: SimpleWebhookCallOptions) => { export const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
const { const {
authentication, authentication,
method, method,
@ -64,15 +65,23 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => {
getVisibleSelect().find('.option-headline').contains(responseData).click(); getVisibleSelect().find('.option-headline').contains(responseData).click();
} }
const callEndpoint = (cb: (response: Cypress.Response<unknown>) => void) => {
cy.request(method, `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then(cb);
};
if (executeNow) { if (executeNow) {
ndv.actions.execute(); ndv.actions.execute();
cy.wait(waitForWebhook); cy.wait(waitForWebhook);
cy.request(method, `${BACKEND_BASE_URL}/webhook-test/${webhookPath}`).then((response) => { callEndpoint((response) => {
expect(response.status).to.eq(200); expect(response.status).to.eq(200);
ndv.getters.outputPanel().contains('headers'); ndv.getters.outputPanel().contains('headers');
}); });
} }
return {
callEndpoint,
};
}; };
describe('Webhook Trigger node', () => { describe('Webhook Trigger node', () => {

View file

@ -1,3 +1,4 @@
import * as projects from '../composables/projects';
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN, NOTION_NODE_NAME } from '../constants'; import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN, NOTION_NODE_NAME } from '../constants';
import { import {
CredentialsModal, CredentialsModal,
@ -7,8 +8,7 @@ import {
WorkflowSharingModal, WorkflowSharingModal,
WorkflowsPage, WorkflowsPage,
} from '../pages'; } from '../pages';
import { getVisibleSelect } from '../utils'; import { getVisibleDropdown, getVisiblePopper, getVisibleSelect } from '../utils';
import * as projects from '../composables/projects';
/** /**
* User U1 - Instance owner * User U1 - Instance owner
@ -135,7 +135,11 @@ describe('Sharing', { disableAutoLogin: true }, () => {
workflowsPage.getters.workflowCards().should('have.length', 2); workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCard('Workflow W1').click(); workflowsPage.getters.workflowCard('Workflow W1').click();
workflowPage.actions.openNode('Notion'); workflowPage.actions.openNode('Notion');
ndv.getters.credentialInput().should('have.value', 'Credential C1').should('be.disabled'); ndv.getters
.credentialInput()
.find('input')
.should('have.value', 'Credential C1')
.should('be.enabled');
ndv.actions.close(); ndv.actions.close();
cy.waitForLoad(); cy.waitForLoad();
@ -176,7 +180,8 @@ describe('Sharing', { disableAutoLogin: true }, () => {
).should('be.visible'); ).should('be.visible');
credentialsModal.getters.usersSelect().click(); credentialsModal.getters.usersSelect().click();
cy.getByTestId('project-sharing-info') getVisiblePopper()
.find('[data-test-id="project-sharing-info"]')
.filter(':visible') .filter(':visible')
.should('have.length', 3) .should('have.length', 3)
.contains(INSTANCE_ADMIN.email) .contains(INSTANCE_ADMIN.email)
@ -188,11 +193,81 @@ describe('Sharing', { disableAutoLogin: true }, () => {
credentialsModal.actions.saveSharing(); credentialsModal.actions.saveSharing();
credentialsModal.actions.close(); credentialsModal.actions.close();
}); });
it('credentials should work between team and personal projects', () => {
cy.resetDatabase();
cy.enableFeature('sharing');
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
cy.signinAsOwner();
cy.visit('/');
projects.createProject('Development');
projects.getHomeButton().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Test workflow');
projects.getHomeButton().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Notion API');
credentialsPage.getters.credentialCard('Notion API').click();
credentialsModal.actions.changeTab('Sharing');
credentialsModal.getters.usersSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 4)
.filter(':contains("Development")')
.should('have.length', 1)
.click();
credentialsModal.getters.saveButton().click();
credentialsModal.getters.saveButton().should('have.text', 'Saved');
credentialsModal.actions.close();
projects.getProjectTabWorkflows().click();
workflowsPage.getters.workflowCardActions('Test workflow').click();
getVisibleDropdown().find('li').contains('Share').click();
workflowSharingModal.getters.usersSelect().filter(':visible').click();
getVisibleSelect().find('li').should('have.length', 3).first().click();
workflowSharingModal.getters.saveButton().click();
projects.getMenuItems().first().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Test workflow 2');
workflowPage.actions.openShareModal();
workflowSharingModal.getters.usersSelect().should('not.exist');
cy.get('body').type('{esc}');
projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.createCredentialButton().click();
projects.createCredential('Notion API 2', false);
credentialsModal.actions.changeTab('Sharing');
credentialsModal.getters.usersSelect().click();
getVisibleSelect().find('li').should('have.length', 4).first().click();
credentialsModal.getters.saveButton().click();
credentialsModal.getters.saveButton().should('have.text', 'Saved');
credentialsModal.actions.close();
credentialsPage.getters
.credentialCards()
.should('have.length', 2)
.filter(':contains("Personal")')
.should('have.length', 1);
});
}); });
describe('Credential Usage in Cross Shared Workflows', () => { describe('Credential Usage in Cross Shared Workflows', () => {
beforeEach(() => { beforeEach(() => {
cy.resetDatabase(); cy.resetDatabase();
cy.enableFeature('sharing');
cy.enableFeature('advancedPermissions'); cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin'); cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor'); cy.enableFeature('projectRole:editor');
@ -203,23 +278,18 @@ describe('Credential Usage in Cross Shared Workflows', () => {
}); });
it('should only show credentials from the same team project', () => { it('should only show credentials from the same team project', () => {
cy.enableFeature('advancedPermissions');
cy.enableFeature('projectRole:admin');
cy.enableFeature('projectRole:editor');
cy.changeQuota('maxTeamProjects', -1);
// Create a notion credential in the home project // Create a notion credential in the home project
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API'); credentialsModal.actions.createNewCredential('Notion API');
// Create a notion credential in one project // Create a notion credential in one project
projects.actions.createProject('Development'); projects.createProject('Development');
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API'); credentialsModal.actions.createNewCredential('Notion API');
// Create a notion credential in another project // Create a notion credential in another project
projects.actions.createProject('Test'); projects.createProject('Test');
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API'); credentialsModal.actions.createNewCredential('Notion API');
@ -235,9 +305,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
}); });
it('should only show credentials in their personal project for members', () => { it('should only show credentials in their personal project for members', () => {
cy.enableFeature('sharing');
cy.reload();
// Create a notion credential as the owner // Create a notion credential as the owner
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.actions.createNewCredential('Notion API'); credentialsModal.actions.createNewCredential('Notion API');
@ -267,8 +334,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
it('should only show credentials in their personal project for members if the workflow was shared with them', () => { it('should only show credentials in their personal project for members if the workflow was shared with them', () => {
const workflowName = 'Test workflow'; const workflowName = 'Test workflow';
cy.enableFeature('sharing');
cy.reload();
// Create a notion credential as the owner and a workflow that is shared // Create a notion credential as the owner and a workflow that is shared
// with member 0 // with member 0
@ -299,7 +364,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => { it("should show all credentials from all personal projects the workflow's been shared into for the global owner", () => {
const workflowName = 'Test workflow'; const workflowName = 'Test workflow';
cy.enableFeature('sharing');
// As member 1, create a new notion credential. This should not show up. // As member 1, create a new notion credential. This should not show up.
cy.signinAsMember(1); cy.signinAsMember(1);
@ -344,8 +408,6 @@ describe('Credential Usage in Cross Shared Workflows', () => {
}); });
it('should show all personal credentials if the global owner owns the workflow', () => { it('should show all personal credentials if the global owner owns the workflow', () => {
cy.enableFeature('sharing');
// As member 0, create a new notion credential. // As member 0, create a new notion credential.
cy.signinAsMember(); cy.signinAsMember();
cy.visit(credentialsPage.url); cy.visit(credentialsPage.url);

View file

@ -1,4 +1,5 @@
import { WorkflowPage } from '../pages'; import { WorkflowPage } from '../pages';
import { getVisibleSelect } from '../utils';
const wf = new WorkflowPage(); const wf = new WorkflowPage();
@ -51,28 +52,6 @@ describe('Workflow tags', () => {
wf.getters.tagPills().should('have.length', 0); // none attached wf.getters.tagPills().should('have.length', 0); // none attached
}); });
it('should update a tag via modal', () => {
wf.actions.openTagManagerModal();
const [first] = TEST_TAGS;
cy.contains('Create a tag').click();
cy.getByTestId('tags-table').find('input').type(first).type('{enter}');
cy.getByTestId('tags-table').should('contain.text', first);
cy.getByTestId('edit-tag-button').eq(-1).click({ force: true });
cy.wait(300);
cy.getByTestId('tags-table')
.find('.el-input--large')
.should('be.visible')
.type(' Updated')
.type('{enter}');
cy.contains('Done').click();
wf.getters.createTagButton().click();
wf.getters.tagsInDropdown().should('have.length', 1); // one stored
wf.getters.tagsInDropdown().contains('Updated').should('exist');
wf.getters.tagPills().should('have.length', 0); // none attached
});
it('should detach a tag inline by clicking on X on tag pill', () => { it('should detach a tag inline by clicking on X on tag pill', () => {
wf.getters.createTagButton().click(); wf.getters.createTagButton().click();
wf.actions.addTags(TEST_TAGS); wf.actions.addTags(TEST_TAGS);
@ -86,10 +65,26 @@ describe('Workflow tags', () => {
it('should detach a tag inline by clicking on dropdown list item', () => { it('should detach a tag inline by clicking on dropdown list item', () => {
wf.getters.createTagButton().click(); wf.getters.createTagButton().click();
wf.actions.addTags(TEST_TAGS); wf.actions.addTags(TEST_TAGS);
wf.getters.nthTagPill(1).click(); wf.getters.workflowTagsContainer().click();
wf.getters.tagsInDropdown().filter('.selected').first().click(); wf.getters.tagsInDropdown().filter('.selected').first().click();
cy.get('body').click(0, 0); cy.get('body').click(0, 0);
wf.getters.workflowTags().click(); wf.getters.workflowTags().click();
wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1); wf.getters.tagPills().should('have.length', TEST_TAGS.length - 1);
}); });
it('should not show non existing tag as a selectable option', () => {
const NON_EXISTING_TAG = 'My Test Tag';
wf.getters.createTagButton().click();
wf.actions.addTags(TEST_TAGS);
cy.get('body').click(0, 0);
wf.getters.workflowTags().click();
wf.getters.workflowTagsInput().type(NON_EXISTING_TAG);
getVisibleSelect()
.find('li')
.should('have.length', 2)
.filter(`:contains("${NON_EXISTING_TAG}")`)
.should('not.have.length');
});
}); });

View file

@ -1,8 +1,8 @@
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN } from '../constants'; import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN } from '../constants';
import { MainSidebar, SettingsSidebar, SettingsUsersPage } from '../pages'; import { MainSidebar, SettingsSidebar, SettingsUsersPage } from '../pages';
import { errorToast, successToast } from '../pages/notifications';
import { PersonalSettingsPage } from '../pages/settings-personal'; import { PersonalSettingsPage } from '../pages/settings-personal';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
import { errorToast, successToast } from '../pages/notifications';
/** /**
* User A - Instance owner * User A - Instance owner

View file

@ -0,0 +1,21 @@
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass();
describe('PAY-1858 context menu', () => {
it('can use context menu on saved workflow', () => {
WorkflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_filter.json', 'test');
WorkflowPage.getters.canvasNodes().should('have.length', 5);
WorkflowPage.actions.deleteNodeFromContextMenu('Then');
WorkflowPage.getters.canvasNodes().should('have.length', 4);
WorkflowPage.actions.hitSaveWorkflow();
cy.reload();
WorkflowPage.getters.canvasNodes().should('have.length', 4);
WorkflowPage.actions.deleteNodeFromContextMenu('Code');
WorkflowPage.getters.canvasNodes().should('have.length', 3);
});
});

View file

@ -1,6 +1,7 @@
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants'; import { SCHEDULE_TRIGGER_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
import { clearNotifications, errorToast, successToast } from '../pages/notifications'; import { clearNotifications, errorToast, successToast } from '../pages/notifications';
import { isCanvasV2 } from '../utils/workflowUtils';
const workflowPage = new WorkflowPageClass(); const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab(); const executionsTab = new WorkflowExecutionsTab();
@ -117,15 +118,22 @@ describe('Execution', () => {
.canvasNodeByName('Manual') .canvasNodeByName('Manual')
.within(() => cy.get('.fa-check')) .within(() => cy.get('.fa-check'))
.should('exist'); .should('exist');
workflowPage.getters
.canvasNodeByName('Wait') if (isCanvasV2()) {
.within(() => cy.get('.fa-sync-alt').should('not.visible')); workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.exist'));
} else {
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.be.visible'));
}
workflowPage.getters workflowPage.getters
.canvasNodeByName('Set') .canvasNodeByName('Set')
.within(() => cy.get('.fa-check').should('not.exist')); .within(() => cy.get('.fa-check').should('not.exist'));
successToast().should('be.visible'); successToast().should('be.visible');
clearNotifications();
// Clear execution data // Clear execution data
workflowPage.getters.clearExecutionDataButton().should('be.visible'); workflowPage.getters.clearExecutionDataButton().should('be.visible');
@ -206,6 +214,7 @@ describe('Execution', () => {
workflowPage.getters.clearExecutionDataButton().should('not.exist'); workflowPage.getters.clearExecutionDataButton().should('not.exist');
}); });
// FIXME: Canvas V2: Webhook should show waiting state but it doesn't
it('should test webhook workflow stop', () => { it('should test webhook workflow stop', () => {
cy.createFixtureWorkflow('Webhook_wait_set.json'); cy.createFixtureWorkflow('Webhook_wait_set.json');
@ -267,15 +276,22 @@ describe('Execution', () => {
.canvasNodeByName('Webhook') .canvasNodeByName('Webhook')
.within(() => cy.get('.fa-check')) .within(() => cy.get('.fa-check'))
.should('exist'); .should('exist');
workflowPage.getters
.canvasNodeByName('Wait') if (isCanvasV2()) {
.within(() => cy.get('.fa-sync-alt').should('not.visible')); workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.exist'));
} else {
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.be.visible'));
}
workflowPage.getters workflowPage.getters
.canvasNodeByName('Set') .canvasNodeByName('Set')
.within(() => cy.get('.fa-check').should('not.exist')); .within(() => cy.get('.fa-check').should('not.exist'));
successToast().should('be.visible'); successToast().should('be.visible');
clearNotifications();
// Clear execution data // Clear execution data
workflowPage.getters.clearExecutionDataButton().should('be.visible'); workflowPage.getters.clearExecutionDataButton().should('be.visible');
@ -296,6 +312,7 @@ describe('Execution', () => {
}); });
}); });
// FIXME: Canvas V2: Missing pinned states for `edge-label-wrapper`
describe('connections should be colored differently for pinned data', () => { describe('connections should be colored differently for pinned data', () => {
beforeEach(() => { beforeEach(() => {
cy.createFixtureWorkflow('Schedule_pinned.json'); cy.createFixtureWorkflow('Schedule_pinned.json');
@ -504,7 +521,7 @@ describe('Execution', () => {
workflowPage.getters.clearExecutionDataButton().should('be.visible'); workflowPage.getters.clearExecutionDataButton().should('be.visible');
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun'); cy.intercept('POST', '/rest/workflows/**/run?**').as('workflowRun');
workflowPage.getters workflowPage.getters
.canvasNodeByName('do something with them') .canvasNodeByName('do something with them')
@ -526,7 +543,7 @@ describe('Execution', () => {
workflowPage.getters.zoomToFitButton().click(); workflowPage.getters.zoomToFitButton().click();
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun'); cy.intercept('POST', '/rest/workflows/**/run?**').as('workflowRun');
workflowPage.getters workflowPage.getters
.canvasNodeByName('If') .canvasNodeByName('If')
@ -548,7 +565,7 @@ describe('Execution', () => {
workflowPage.getters.clearExecutionDataButton().should('be.visible'); workflowPage.getters.clearExecutionDataButton().should('be.visible');
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun'); cy.intercept('POST', '/rest/workflows/**/run?**').as('workflowRun');
workflowPage.getters workflowPage.getters
.canvasNodeByName('NoOp2') .canvasNodeByName('NoOp2')
@ -577,7 +594,7 @@ describe('Execution', () => {
it('should successfully execute partial executions with nodes attached to the second output', () => { it('should successfully execute partial executions with nodes attached to the second output', () => {
cy.createFixtureWorkflow('Test_Workflow_pairedItem_incomplete_manual_bug.json'); cy.createFixtureWorkflow('Test_Workflow_pairedItem_incomplete_manual_bug.json');
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun'); cy.intercept('POST', '/rest/workflows/**/run?**').as('workflowRun');
workflowPage.getters.zoomToFitButton().click(); workflowPage.getters.zoomToFitButton().click();
workflowPage.getters.executeWorkflowButton().click(); workflowPage.getters.executeWorkflowButton().click();
@ -597,7 +614,7 @@ describe('Execution', () => {
it('should execute workflow partially up to the node that has issues', () => { it('should execute workflow partially up to the node that has issues', () => {
cy.createFixtureWorkflow('Test_workflow_partial_execution_with_missing_credentials.json'); cy.createFixtureWorkflow('Test_workflow_partial_execution_with_missing_credentials.json');
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun'); cy.intercept('POST', '/rest/workflows/**/run?**').as('workflowRun');
workflowPage.getters.zoomToFitButton().click(); workflowPage.getters.zoomToFitButton().click();
workflowPage.getters.executeWorkflowButton().click(); workflowPage.getters.executeWorkflowButton().click();
@ -617,4 +634,45 @@ describe('Execution', () => {
errorToast().should('contain', 'Problem in node Telegram'); errorToast().should('contain', 'Problem in node Telegram');
}); });
it('should not show pinned data in production execution', () => {
cy.createFixtureWorkflow('Execution-pinned-data-check.json');
workflowPage.getters.zoomToFitButton().click();
cy.intercept('PATCH', '/rest/workflows/*').as('workflowActivate');
workflowPage.getters.activatorSwitch().click();
cy.wait('@workflowActivate');
cy.get('body').type('{esc}');
workflowPage.actions.openNode('Webhook');
cy.contains('label', 'Production URL').should('be.visible').click();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
cy.get('.webhook-url').click();
ndv.getters.backToCanvas().click();
cy.readClipboard().then((url) => {
cy.request({
method: 'GET',
url,
}).then((resp) => {
expect(resp.status).to.eq(200);
});
});
cy.intercept('GET', '/rest/executions/*').as('getExecution');
executionsTab.actions.switchToExecutionsTab();
cy.wait('@getExecution');
executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find('.connection-run-items-label')
.filter(':contains("5 items")')
.should('have.length', 2);
});
}); });

View file

@ -1,5 +1,8 @@
import type { ICredentialType } from 'n8n-workflow'; import { type ICredentialType } from 'n8n-workflow';
import { import {
AGENT_NODE_NAME,
AI_TOOL_HTTP_NODE_NAME,
GMAIL_NODE_NAME, GMAIL_NODE_NAME,
HTTP_REQUEST_NODE_NAME, HTTP_REQUEST_NODE_NAME,
NEW_GOOGLE_ACCOUNT_NAME, NEW_GOOGLE_ACCOUNT_NAME,
@ -12,7 +15,7 @@ import {
TRELLO_NODE_NAME, TRELLO_NODE_NAME,
} from '../constants'; } from '../constants';
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages'; import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
import { successToast } from '../pages/notifications'; import { errorToast, successToast } from '../pages/notifications';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
const credentialsPage = new CredentialsPage(); const credentialsPage = new CredentialsPage();
@ -110,13 +113,13 @@ describe('Credentials', () => {
workflowPage.getters.nodeCredentialsSelect().should('have.length', 2); workflowPage.getters.nodeCredentialsSelect().should('have.length', 2);
workflowPage.getters.nodeCredentialsSelect().first().click(); workflowPage.getters.nodeCredentialsSelect().first().click();
getVisibleSelect().find('li').last().click(); getVisibleSelect().find('li').contains('Create New Credential').click();
// This one should show auth type selector // This one should show auth type selector
credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2); credentialsModal.getters.credentialAuthTypeRadioButtons().should('have.length', 2);
cy.get('body').type('{esc}'); cy.get('body').type('{esc}');
workflowPage.getters.nodeCredentialsSelect().last().click(); workflowPage.getters.nodeCredentialsSelect().last().click();
getVisibleSelect().find('li').last().click(); getVisibleSelect().find('li').contains('Create New Credential').click();
// This one should not show auth type selector // This one should not show auth type selector
credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist'); credentialsModal.getters.credentialsAuthTypeSelector().should('not.exist');
}); });
@ -201,6 +204,31 @@ describe('Credentials', () => {
.should('have.value', NEW_CREDENTIAL_NAME2); .should('have.value', NEW_CREDENTIAL_NAME2);
}); });
it('should edit credential for non-standard credential type', () => {
workflowPage.actions.visit();
workflowPage.actions.addNodeToCanvas(AGENT_NODE_NAME);
workflowPage.actions.addNodeToCanvas(AI_TOOL_HTTP_NODE_NAME);
workflowPage.getters.canvasNodes().last().click();
cy.get('body').type('{enter}');
cy.getByTestId('parameter-input-authentication').click();
cy.contains('Predefined Credential Type').click();
cy.getByTestId('credential-select').click();
cy.contains('Adalo API').click();
workflowPage.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
credentialsModal.actions.fillCredentialsForm();
workflowPage.getters.nodeCredentialsEditButton().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
credentialsModal.getters.name().click();
credentialsModal.actions.renameCredential(NEW_CREDENTIAL_NAME);
credentialsModal.getters.saveButton().click();
credentialsModal.getters.closeButton().click();
workflowPage.getters
.nodeCredentialsSelect()
.find('input')
.should('have.value', NEW_CREDENTIAL_NAME);
});
it('should setup generic authentication for HTTP node', () => { it('should setup generic authentication for HTTP node', () => {
workflowPage.actions.visit(); workflowPage.actions.visit();
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
@ -250,4 +278,25 @@ describe('Credentials', () => {
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click(); credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
nodeDetailsView.getters.copyInput().should('not.exist'); nodeDetailsView.getters.copyInput().should('not.exist');
}); });
it('ADO-2583 should show notifications above credential modal overlay', () => {
// check error notifications because they are sticky
cy.intercept('POST', '/rest/credentials', { forceNetworkError: true });
credentialsPage.getters.createCredentialButton().click();
credentialsModal.getters.newCredentialModal().should('be.visible');
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
credentialsModal.getters.newCredentialTypeButton().click();
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
credentialsModal.actions.setName('My awesome Notion account');
credentialsModal.getters.saveButton().click({ force: true });
errorToast().should('have.length', 1);
errorToast().should('be.visible');
errorToast().should('have.css', 'z-index', '2100');
cy.get('.el-overlay').should('have.css', 'z-index', '2001');
});
}); });

View file

@ -1,7 +1,8 @@
import type { RouteHandler } from 'cypress/types/net-stubbing'; import type { RouteHandler } from 'cypress/types/net-stubbing';
import executionOutOfMemoryServerResponse from '../fixtures/responses/execution-out-of-memory-server-response.json';
import { WorkflowPage } from '../pages'; import { WorkflowPage } from '../pages';
import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab'; import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab';
import executionOutOfMemoryServerResponse from '../fixtures/responses/execution-out-of-memory-server-response.json';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
@ -9,218 +10,281 @@ const executionsTab = new WorkflowExecutionsTab();
const executionsRefreshInterval = 4000; const executionsRefreshInterval = 4000;
// Test suite for executions tab // Test suite for executions tab
describe('Current Workflow Executions', () => { describe('Workflow Executions', () => {
beforeEach(() => { describe('when workflow is saved', () => {
workflowPage.actions.visit(); beforeEach(() => {
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow'); workflowPage.actions.visit();
}); cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
});
it('should render executions tab correctly', () => { it('should render executions tab correctly', () => {
createMockExecutions(); createMockExecutions();
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
executionsTab.actions.switchToExecutionsTab(); executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions']); cy.wait(['@getExecutions']);
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0); executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
executionsTab.getters.executionListItems().should('have.length', 11); executionsTab.getters.executionListItems().should('have.length', 11);
executionsTab.getters.successfulExecutionListItems().should('have.length', 9); executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
executionsTab.getters.failedExecutionListItems().should('have.length', 2); executionsTab.getters.failedExecutionListItems().should('have.length', 2);
executionsTab.getters executionsTab.getters
.executionListItems() .executionListItems()
.first() .first()
.invoke('attr', 'class') .invoke('attr', 'class')
.should('match', /_active_/); .should('match', /_active_/);
}); });
it('should not redirect back to execution tab when request is not done before leaving the page', () => { it('should not redirect back to execution tab when request is not done before leaving the page', () => {
cy.intercept('GET', '/rest/executions?filter=*'); cy.intercept('GET', '/rest/executions?filter=*');
cy.intercept('GET', '/rest/executions/active?filter=*'); cy.intercept('GET', '/rest/executions/active?filter=*');
executionsTab.actions.switchToExecutionsTab(); executionsTab.actions.switchToExecutionsTab();
executionsTab.actions.switchToEditorTab(); executionsTab.actions.switchToEditorTab();
cy.wait(executionsRefreshInterval); cy.wait(executionsRefreshInterval);
cy.url().should('not.include', '/executions'); cy.url().should('not.include', '/executions');
executionsTab.actions.switchToExecutionsTab(); executionsTab.actions.switchToExecutionsTab();
executionsTab.actions.switchToEditorTab(); executionsTab.actions.switchToEditorTab();
executionsTab.actions.switchToExecutionsTab(); executionsTab.actions.switchToExecutionsTab();
executionsTab.actions.switchToEditorTab(); executionsTab.actions.switchToEditorTab();
executionsTab.actions.switchToExecutionsTab(); executionsTab.actions.switchToExecutionsTab();
executionsTab.actions.switchToEditorTab(); executionsTab.actions.switchToEditorTab();
cy.wait(executionsRefreshInterval); cy.wait(executionsRefreshInterval);
cy.url().should('not.include', '/executions'); cy.url().should('not.include', '/executions');
executionsTab.actions.switchToExecutionsTab(); executionsTab.actions.switchToExecutionsTab();
cy.wait(1000); cy.wait(1000);
executionsTab.actions.switchToEditorTab(); executionsTab.actions.switchToEditorTab();
cy.wait(executionsRefreshInterval); cy.wait(executionsRefreshInterval);
cy.url().should('not.include', '/executions'); cy.url().should('not.include', '/executions');
}); });
it('should not redirect back to execution tab when slow request is not done before leaving the page', () => { it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
const throttleResponse: RouteHandler = async (req) => { const throttleResponse: RouteHandler = async (req) => {
return await new Promise((resolve) => { return await new Promise((resolve) => {
setTimeout(() => resolve(req.continue()), 2000); setTimeout(() => resolve(req.continue()), 2000);
});
};
cy.intercept('GET', '/rest/executions?filter=*', throttleResponse);
cy.intercept('GET', '/rest/executions/active?filter=*', throttleResponse);
executionsTab.actions.switchToExecutionsTab();
executionsTab.actions.switchToEditorTab();
cy.wait(executionsRefreshInterval);
cy.url().should('not.include', '/executions');
});
it('should error toast when server error message returned without stack trace', () => {
executionsTab.actions.createManualExecutions(1);
const message = 'Workflow did not finish, possible out-of-memory issue';
cy.intercept('GET', '/rest/executions/*', {
statusCode: 200,
body: executionOutOfMemoryServerResponse,
}).as('getExecution');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecution']);
executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body') // Access the body of the iframe document
.should('not.be.empty') // Ensure the body is not empty
.then(cy.wrap)
.find('.el-notification:has(.el-notification--error)')
.should('be.visible')
.filter(`:contains("${message}")`)
.should('be.visible');
});
it('should show workflow data in executions tab after hard reload and modify name and tags', () => {
executionsTab.actions.switchToExecutionsTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
workflowPage.getters.workflowTags().click();
getVisibleSelect().find('li:contains("Manage tags")').click();
cy.get('button:contains("Add new")').click();
cy.getByTestId('tags-table').find('input').type('nutag').type('{enter}');
cy.get('button:contains("Done")').click();
cy.reload();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.workflowTags().click();
workflowPage.getters.tagsInDropdown().first().should('have.text', 'nutag').click();
workflowPage.getters.tagPills().should('have.length', 3);
let newWorkflowName = 'Renamed workflow';
workflowPage.actions.renameWorkflow(newWorkflowName);
workflowPage.getters.isWorkflowSaved();
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToEditorTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 3);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToExecutionsTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 3);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToEditorTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 3);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
newWorkflowName = 'New workflow';
workflowPage.actions.renameWorkflow(newWorkflowName);
workflowPage.getters.isWorkflowSaved();
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
workflowPage.getters.workflowTags().click();
workflowPage.getters.tagsDropdown().find('.el-tag__close').first().click();
cy.get('body').click(0, 0);
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
executionsTab.actions.switchToExecutionsTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToEditorTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
});
it('should load items and auto scroll after filter change', () => {
createMockExecutions();
createMockExecutions();
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions']);
executionsTab.getters.executionsList().scrollTo(0, 500).wait(0);
executionsTab.getters.executionListItems().eq(10).click();
cy.getByTestId('executions-filter-button').click();
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
getVisibleSelect().find('li:contains("Error")').click();
executionsTab.getters.executionListItems().should('have.length', 5);
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
executionsTab.getters.failedExecutionListItems().should('have.length', 4);
cy.getByTestId('executions-filter-button').click();
cy.getByTestId('executions-filter-status-select').should('be.visible').click();
getVisibleSelect().find('li:contains("Success")').click();
// check if the list is scrolled
executionsTab.getters.executionListItems().eq(10).should('be.visible');
executionsTab.getters.executionsList().then(($el) => {
const { scrollTop, scrollHeight, clientHeight } = $el[0];
expect(scrollTop).to.be.greaterThan(0);
expect(scrollTop + clientHeight).to.be.lessThan(scrollHeight);
// scroll to the bottom
$el[0].scrollTo(0, scrollHeight);
executionsTab.getters.executionListItems().should('have.length', 18);
executionsTab.getters.successfulExecutionListItems().should('have.length', 18);
executionsTab.getters.failedExecutionListItems().should('have.length', 0);
}); });
};
cy.intercept('GET', '/rest/executions?filter=*', throttleResponse); cy.getByTestId('executions-filter-button').click();
cy.intercept('GET', '/rest/executions/active?filter=*', throttleResponse); cy.getByTestId('executions-filter-reset-button').should('be.visible').click();
executionsTab.getters.executionListItems().eq(11).should('be.visible');
});
executionsTab.actions.switchToExecutionsTab(); it('should redirect back to editor after seeing a couple of execution using browser back button', () => {
executionsTab.actions.switchToEditorTab(); createMockExecutions();
cy.wait(executionsRefreshInterval); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.url().should('not.include', '/executions');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions']);
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
executionsTab.getters.executionListItems().eq(2).click();
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
executionsTab.getters.executionListItems().eq(4).click();
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
executionsTab.getters.executionListItems().eq(6).click();
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
cy.go('back');
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
cy.go('back');
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
cy.go('back');
executionsTab.getters.workflowExecutionPreviewIframe().should('exist');
cy.go('back');
cy.url().should('not.include', '/executions');
cy.url().should('include', '/workflow/');
workflowPage.getters.nodeViewRoot().should('be.visible');
});
}); });
it('should error toast when server error message returned without stack trace', () => { describe('when new workflow is not saved', () => {
executionsTab.actions.createManualExecutions(1); beforeEach(() => {
const message = 'Workflow did not finish, possible out-of-memory issue'; workflowPage.actions.visit();
cy.intercept('GET', '/rest/executions/*', { });
statusCode: 200,
body: executionOutOfMemoryServerResponse,
}).as('getExecution');
executionsTab.actions.switchToExecutionsTab(); it('should open executions tab', () => {
cy.wait(['@getExecution']); executionsTab.actions.switchToExecutionsTab();
executionsTab.getters.executionsSidebar().should('be.visible');
executionsTab.getters.executionsEmptyList().should('be.visible');
cy.getByTestId('workflow-execution-no-trigger-content').should('be.visible');
cy.get('button:contains("Add first step")').should('be.visible').click();
executionsTab.getters cy.getByTestId('node-creator-item-name')
.workflowExecutionPreviewIframe() .should('be.visible')
.should('be.visible') .filter(':contains("Trigger")')
.its('0.contentDocument.body') // Access the body of the iframe document .click();
.should('not.be.empty') // Ensure the body is not empty executionsTab.actions.switchToExecutionsTab();
executionsTab.getters.executionsSidebar().should('be.visible');
executionsTab.getters.executionsEmptyList().should('be.visible');
cy.getByTestId('workflow-execution-no-content').should('be.visible');
.then(cy.wrap) workflowPage.getters.saveButton().find('button').should('be.enabled').click();
.find('.el-notification:has(.el-notification--error)') workflowPage.getters.isWorkflowSaved();
.should('be.visible') workflowPage.getters.nodeViewRoot().should('be.visible');
.filter(`:contains("${message}")`) });
.should('be.visible');
});
it('should auto load more items if there is space and auto scroll', () => {
cy.viewport(1280, 960);
executionsTab.actions.createManualExecutions(24);
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions']);
executionsTab.getters.executionListItems().its('length').should('be.gte', 10);
cy.getByTestId('current-executions-list').scrollTo('bottom');
cy.wait(['@getExecutions']);
executionsTab.getters.executionListItems().should('have.length', 24);
executionsTab.getters.executionListItems().eq(14).click();
cy.wait(['@getExecution']);
cy.reload();
cy.wait(['@getExecutions']);
executionsTab.getters.executionListItems().eq(14).should('not.be.visible');
executionsTab.getters.executionListItems().should('have.length', 24);
executionsTab.getters.executionListItems().first().should('not.be.visible');
cy.getByTestId('current-executions-list').scrollTo(0, 0);
executionsTab.getters.executionListItems().first().should('be.visible');
executionsTab.getters.executionListItems().eq(14).should('not.be.visible');
executionsTab.actions.switchToEditorTab();
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions']);
executionsTab.getters.executionListItems().eq(14).should('not.be.visible');
executionsTab.getters.executionListItems().should('have.length', 24);
executionsTab.getters.executionListItems().first().should('not.be.visible');
cy.getByTestId('current-executions-list').scrollTo(0, 0);
executionsTab.getters.executionListItems().first().should('be.visible');
executionsTab.getters.executionListItems().eq(14).should('not.be.visible');
});
it('should show workflow data in executions tab after hard reload and modify name and tags', () => {
executionsTab.actions.switchToExecutionsTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
workflowPage.getters.workflowTags().click();
getVisibleSelect().find('li:contains("Manage tags")').click();
cy.get('button:contains("Add new")').click();
cy.getByTestId('tags-table').find('input').type('nutag').type('{enter}');
cy.get('button:contains("Done")').click();
cy.reload();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.workflowTags().click();
workflowPage.getters.tagsInDropdown().first().should('have.text', 'nutag').click();
workflowPage.getters.tagPills().should('have.length', 3);
let newWorkflowName = 'Renamed workflow';
workflowPage.actions.renameWorkflow(newWorkflowName);
workflowPage.getters.isWorkflowSaved();
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToEditorTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 3);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToExecutionsTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 3);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToEditorTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 3);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
newWorkflowName = 'New workflow';
workflowPage.actions.renameWorkflow(newWorkflowName);
workflowPage.getters.isWorkflowSaved();
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
workflowPage.getters.workflowTags().click();
workflowPage.getters.tagsDropdown().find('.el-tag__close').first().click();
cy.get('body').click(0, 0);
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
executionsTab.actions.switchToExecutionsTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
executionsTab.actions.switchToEditorTab();
checkMainHeaderELements();
workflowPage.getters.saveButton().find('button').should('not.exist');
workflowPage.getters.tagPills().should('have.length', 2);
workflowPage.getters
.workflowNameInputContainer()
.invoke('attr', 'title')
.should('eq', newWorkflowName);
}); });
}); });
@ -228,9 +292,11 @@ const createMockExecutions = () => {
executionsTab.actions.createManualExecutions(5); executionsTab.actions.createManualExecutions(5);
// Make some failed executions by enabling Code node with syntax error // Make some failed executions by enabling Code node with syntax error
executionsTab.actions.toggleNodeEnabled('Error'); executionsTab.actions.toggleNodeEnabled('Error');
workflowPage.getters.disabledNodes().should('have.length', 0);
executionsTab.actions.createManualExecutions(2); executionsTab.actions.createManualExecutions(2);
// Then add some more successful ones // Then add some more successful ones
executionsTab.actions.toggleNodeEnabled('Error'); executionsTab.actions.toggleNodeEnabled('Error');
workflowPage.getters.disabledNodes().should('have.length', 1);
executionsTab.actions.createManualExecutions(4); executionsTab.actions.createManualExecutions(4);
}; };

View file

@ -1,10 +1,18 @@
import type { ICredentialType } from 'n8n-workflow'; import type { ICredentialType } from 'n8n-workflow';
import { NodeCreator } from '../pages/features/node-creator';
import CustomNodeFixture from '../fixtures/Custom_node.json';
import { CredentialsModal, WorkflowPage } from '../pages';
import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json';
import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
import CustomCredential from '../fixtures/Custom_credential.json'; import CustomCredential from '../fixtures/Custom_credential.json';
import CustomNodeFixture from '../fixtures/Custom_node.json';
import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json';
import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json';
import { CredentialsModal, WorkflowPage } from '../pages';
import { NodeCreator } from '../pages/features/node-creator';
import {
confirmCommunityNodeUninstall,
confirmCommunityNodeUpdate,
getCommunityCards,
installFirstCommunityNode,
visitCommunityNodesSettings,
} from '../pages/settings-community-nodes';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
const credentialsModal = new CredentialsModal(); const credentialsModal = new CredentialsModal();
@ -14,7 +22,7 @@ const workflowPage = new WorkflowPage();
// We separate-out the custom nodes because they require injecting nodes and credentials // We separate-out the custom nodes because they require injecting nodes and credentials
// so the /nodes and /credentials endpoints are intercepted and non-cached. // so the /nodes and /credentials endpoints are intercepted and non-cached.
// We want to keep the other tests as fast as possible so we don't want to break the cache in those. // We want to keep the other tests as fast as possible so we don't want to break the cache in those.
describe('Community Nodes', () => { describe('Community and custom nodes in canvas', () => {
beforeEach(() => { beforeEach(() => {
cy.intercept('/types/nodes.json', { middleware: true }, (req) => { cy.intercept('/types/nodes.json', { middleware: true }, (req) => {
req.headers['cache-control'] = 'no-cache, no-store'; req.headers['cache-control'] = 'no-cache, no-store';
@ -95,3 +103,89 @@ describe('Community Nodes', () => {
credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential'); credentialsModal.getters.editCredentialModal().should('contain.text', 'Custom E2E Credential');
}); });
}); });
describe('Community nodes', () => {
const mockPackage = {
createdAt: '2024-07-22T19:08:06.505Z',
updatedAt: '2024-07-22T19:08:06.505Z',
packageName: 'n8n-nodes-chatwork',
installedVersion: '1.0.0',
authorName: null,
authorEmail: null,
installedNodes: [
{
name: 'Chatwork',
type: 'n8n-nodes-chatwork.chatwork',
latestVersion: 1,
},
],
updateAvailable: '1.1.2',
};
it('can install, update and uninstall community nodes', () => {
cy.intercept(
{
hostname: 'api.npms.io',
pathname: '/v2/search',
query: { q: 'keywords:n8n-community-node-package' },
},
{ body: {} },
);
cy.intercept(
{ method: 'GET', pathname: '/rest/community-packages', times: 1 },
{
body: { data: [] },
},
).as('getEmptyPackages');
visitCommunityNodesSettings();
cy.wait('@getEmptyPackages');
// install a package
cy.intercept(
{ method: 'POST', pathname: '/rest/community-packages', times: 1 },
{
body: { data: mockPackage },
},
).as('installPackage');
cy.intercept(
{ method: 'GET', pathname: '/rest/community-packages', times: 1 },
{
body: { data: [mockPackage] },
},
).as('getPackages');
installFirstCommunityNode('n8n-nodes-chatwork@1.0.0');
cy.wait('@installPackage');
cy.wait('@getPackages');
getCommunityCards().should('have.length', 1);
getCommunityCards().eq(0).should('include.text', 'v1.0.0');
// update the package
cy.intercept(
{ method: 'PATCH', pathname: '/rest/community-packages' },
{
body: { data: { ...mockPackage, installedVersion: '1.2.0', updateAvailable: undefined } },
},
).as('updatePackage');
getCommunityCards().eq(0).find('button').click();
confirmCommunityNodeUpdate();
cy.wait('@updatePackage');
getCommunityCards().should('have.length', 1);
getCommunityCards().eq(0).should('not.include.text', 'v1.0.0');
// uninstall the package
cy.intercept(
{
method: 'DELETE',
pathname: '/rest/community-packages',
query: { name: 'n8n-nodes-chatwork' },
},
{ statusCode: 204 },
).as('uninstallPackage');
getCommunityCards().getByTestId('action-toggle').click();
cy.getByTestId('action-uninstall').click();
confirmCommunityNodeUninstall();
cy.wait('@uninstallPackage');
cy.getByTestId('action-box').should('exist');
});
});

View file

@ -1,5 +1,4 @@
import { WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages'; import { WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
import { BACKEND_BASE_URL } from '../constants';
const workflowPage = new WorkflowPageClass(); const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab(); const executionsTab = new WorkflowExecutionsTab();
@ -17,33 +16,6 @@ describe('ADO-2106 connections should be colored correctly for pinned data in ex
workflowPage.getters.getConnectionBetweenNodes('Webhook', 'Set').should('have.class', 'pinned'); workflowPage.getters.getConnectionBetweenNodes('Webhook', 'Set').should('have.class', 'pinned');
}); });
it('should not color connections for pinned data nodes for production executions', () => {
workflowPage.actions.activateWorkflow();
// Execute the workflow
cy.request('POST', `${BACKEND_BASE_URL}/webhook/23fc3930-b8f9-41d9-89db-b647291a2201`, {
here: 'is some data',
}).then((response) => {
expect(response.status).to.eq(200);
});
executionsTab.actions.switchToExecutionsTab();
executionsTab.getters.successfulExecutionListItems().should('have.length', 1);
executionsTab.getters
.workflowExecutionPreviewIframe()
.should('be.visible')
.its('0.contentDocument.body')
.should('not.be.empty')
.then(cy.wrap)
.find('.jtk-connector[data-source-node="Webhook"][data-target-node="Set"]')
.should('have.class', 'success')
.should('have.class', 'has-run')
.should('not.have.class', 'pinned');
});
it('should color connections for pinned data nodes for manual executions', () => { it('should color connections for pinned data nodes for manual executions', () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();

View file

@ -0,0 +1,280 @@
import type { ExecutionError } from 'n8n-workflow/src';
import {
closeManualChatModal,
getManualChatMessages,
getManualChatModalLogs,
getManualChatModalLogsEntries,
sendManualChatMessage,
} from '../composables/modals/chat-modal';
import { setCredentialValues } from '../composables/modals/credential-modal';
import {
clickCreateNewCredential,
clickExecuteNode,
clickGetBackToCanvas,
} from '../composables/ndv';
import {
addLanguageModelNodeToParent,
addMemoryNodeToParent,
addNodeToCanvas,
addToolNodeToParent,
navigateToNewWorkflowPage,
openNode,
} from '../composables/workflow';
import {
AGENT_NODE_NAME,
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AI_MEMORY_POSTGRES_NODE_NAME,
AI_TOOL_CALCULATOR_NODE_NAME,
CHAT_TRIGGER_NODE_DISPLAY_NAME,
MANUAL_CHAT_TRIGGER_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
MANUAL_TRIGGER_NODE_NAME,
} from '../constants';
import { NDV, WorkflowPage as WorkflowPageClass } from '../pages';
import { createMockNodeExecutionData, getVisibleSelect, runMockWorkflowExecution } from '../utils';
const ndv = new NDV();
const WorkflowPage = new WorkflowPageClass();
function createRunDataWithError(inputMessage: string) {
return [
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
jsonData: {
main: { input: inputMessage },
},
}),
createMockNodeExecutionData(AI_MEMORY_POSTGRES_NODE_NAME, {
jsonData: {
ai_memory: {
json: {
action: 'loadMemoryVariables',
values: {
input: inputMessage,
system_message: 'You are a helpful assistant',
formatting_instructions:
'IMPORTANT: Always call `format_final_response` to format your final response!',
},
},
},
},
inputOverride: {
ai_memory: [
[
{
json: {
action: 'loadMemoryVariables',
values: {
input: inputMessage,
system_message: 'You are a helpful assistant',
formatting_instructions:
'IMPORTANT: Always call `format_final_response` to format your final response!',
},
},
},
],
],
},
error: {
message: 'Internal error',
timestamp: 1722591723244,
name: 'NodeOperationError',
description: 'Internal error',
context: {},
cause: {
name: 'error',
severity: 'FATAL',
code: '3D000',
file: 'postinit.c',
line: '885',
routine: 'InitPostgres',
} as unknown as Error,
} as ExecutionError,
}),
createMockNodeExecutionData(AGENT_NODE_NAME, {
executionStatus: 'error',
error: {
level: 'error',
tags: {
packageName: 'workflow',
},
context: {},
functionality: 'configuration-node',
name: 'NodeOperationError',
timestamp: 1722591723244,
node: {
parameters: {
notice: '',
sessionIdType: 'fromInput',
tableName: 'n8n_chat_histories',
},
id: '6b9141da-0135-4e9d-94d1-2d658cbf48b5',
name: 'Postgres Chat Memory',
type: '@n8n/n8n-nodes-langchain.memoryPostgresChat',
typeVersion: 1,
position: [1140, 500],
credentials: {
postgres: {
id: 'RkyZetVpGsSfEAhQ',
name: 'Postgres account',
},
},
},
messages: ['database "chat11" does not exist'],
description: 'Internal error',
message: 'Internal error',
} as unknown as ExecutionError,
metadata: {
subRun: [
{
node: 'Postgres Chat Memory',
runIndex: 0,
},
],
},
}),
];
}
function setupTestWorkflow(chatTrigger: boolean = false) {
// Setup test workflow with AI Agent, Postgres Memory Node (source of error), Calculator Tool, and OpenAI Chat Model
if (chatTrigger) {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
} else {
addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
}
addNodeToCanvas(AGENT_NODE_NAME, true);
if (!chatTrigger) {
// Remove chat trigger
WorkflowPage.getters
.canvasNodeByName(CHAT_TRIGGER_NODE_DISPLAY_NAME)
.find('[data-test-id="delete-node-button"]')
.click({ force: true });
// Set manual trigger to output standard pinned data
openNode(MANUAL_TRIGGER_NODE_DISPLAY_NAME);
ndv.actions.editPinnedData();
ndv.actions.savePinnedData();
ndv.actions.close();
}
// Calculator is added just to make OpenAI Chat Model work (tools can not be empty with OpenAI model)
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
clickGetBackToCanvas();
addMemoryNodeToParent(AI_MEMORY_POSTGRES_NODE_NAME, AGENT_NODE_NAME);
clickCreateNewCredential();
setCredentialValues({
password: 'testtesttest',
});
ndv.getters.parameterInput('sessionIdType').click();
getVisibleSelect().contains('Define below').click();
ndv.getters.parameterInput('sessionKey').type('asdasd');
clickGetBackToCanvas();
addLanguageModelNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
AGENT_NODE_NAME,
true,
);
clickCreateNewCredential();
setCredentialValues({
apiKey: 'sk_test_123',
});
clickGetBackToCanvas();
WorkflowPage.actions.zoomToFit();
}
function checkMessages(inputMessage: string, outputMessage: string) {
const messages = getManualChatMessages();
messages.should('have.length', 2);
messages.should('contain', inputMessage);
messages.should('contain', outputMessage);
getManualChatModalLogs().should('exist');
getManualChatModalLogsEntries()
.should('have.length', 1)
.should('contain', AI_MEMORY_POSTGRES_NODE_NAME);
}
describe("AI-233 Make root node's logs pane active in case of an error in sub-nodes", () => {
beforeEach(() => {
navigateToNewWorkflowPage();
});
it('should open logs tab by default when there was an error', () => {
setupTestWorkflow(true);
openNode(AGENT_NODE_NAME);
const inputMessage = 'Test the code tool';
clickExecuteNode();
runMockWorkflowExecution({
trigger: () => sendManualChatMessage(inputMessage),
runData: createRunDataWithError(inputMessage),
lastNodeExecuted: AGENT_NODE_NAME,
});
checkMessages(inputMessage, '[ERROR: Internal error]');
closeManualChatModal();
// Open the AI Agent node to see the logs
openNode(AGENT_NODE_NAME);
// Finally check that logs pane is opened by default
ndv.getters.outputDataContainer().should('be.visible');
ndv.getters.aiOutputModeToggle().should('be.visible');
ndv.getters
.aiOutputModeToggle()
.find('[role="radio"]')
.should('have.length', 2)
.eq(1)
.should('have.attr', 'aria-checked', 'true');
ndv.getters
.outputPanel()
.findChildByTestId('node-error-message')
.should('be.visible')
.should('contain', 'Error in sub-node');
});
it('should switch to logs tab on error, when NDV is already opened', () => {
setupTestWorkflow(false);
openNode(AGENT_NODE_NAME);
const inputMessage = 'Test the code tool';
runMockWorkflowExecution({
trigger: () => clickExecuteNode(),
runData: createRunDataWithError(inputMessage),
lastNodeExecuted: AGENT_NODE_NAME,
});
// Check that logs pane is opened by default
ndv.getters.outputDataContainer().should('be.visible');
ndv.getters.aiOutputModeToggle().should('be.visible');
ndv.getters
.aiOutputModeToggle()
.find('[role="radio"]')
.should('have.length', 2)
.eq(1)
.should('have.attr', 'aria-checked', 'true');
ndv.getters
.outputPanel()
.findChildByTestId('node-error-message')
.should('be.visible')
.should('contain', 'Error in sub-node');
});
});

View file

@ -0,0 +1,86 @@
import { NDV, WorkflowPage } from '../pages';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
describe('ADO-2362 ADO-2350 NDV Prevent clipping long parameters and scrolling to expression', () => {
it('should show last parameters and open at scroll top of parameters', () => {
workflowPage.actions.visit();
cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json');
workflowPage.actions.openNode('Schedule Trigger');
ndv.getters.inlineExpressionEditorInput().should('be.visible');
ndv.actions.close();
workflowPage.actions.openNode('Edit Fields1');
// first parameter should be visible
ndv.getters.inputLabel().eq(0).should('include.text', 'Mode');
ndv.getters.inputLabel().eq(0).should('be.visible');
ndv.getters.inlineExpressionEditorInput().should('have.length', 2);
// last parameter in view should be visible
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible!');
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
// next parameter in view should not be visible
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
ndv.actions.close();
workflowPage.actions.openNode('Schedule Trigger');
// first parameter (notice) should be visible
ndv.getters.nthParam(0).should('include.text', 'This workflow will run on the schedule ');
ndv.getters.inputLabel().eq(0).should('be.visible');
ndv.getters.inlineExpressionEditorInput().should('have.length', 2);
// last parameter in view should be visible
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible');
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
// next parameter in view should not be visible
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
ndv.actions.close();
workflowPage.actions.openNode('Slack');
// first field (credentials) should be visible
ndv.getters.nodeCredentialsLabel().should('be.visible');
// last parameter in view should be visible
ndv.getters.inlineExpressionEditorInput().eq(0).should('have.text', 'should be visible');
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
// next parameter in view should not be visible
ndv.getters.inlineExpressionEditorInput().eq(1).should('have.text', 'not visible');
ndv.getters.inlineExpressionEditorInput().eq(1).should('not.be.visible');
});
it('NODE-1272 ensure expressions scrolled to top, not middle', () => {
workflowPage.actions.visit();
cy.createFixtureWorkflow('Test-workflow-with-long-parameters.json');
workflowPage.actions.openNode('With long expression');
ndv.getters.inlineExpressionEditorInput().eq(0).should('be.visible');
// should be scrolled at top
ndv.getters
.inlineExpressionEditorInput()
.eq(0)
.find('.cm-line')
.eq(0)
.should('have.text', '1 visible!');
ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(0).should('be.visible');
ndv.getters
.inlineExpressionEditorInput()
.eq(0)
.find('.cm-line')
.eq(6)
.should('have.text', '7 not visible!');
ndv.getters.inlineExpressionEditorInput().eq(0).find('.cm-line').eq(6).should('not.be.visible');
});
});

View file

@ -162,64 +162,6 @@ describe('NDV', () => {
ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item'); ndv.getters.inputTableRow(3).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
}); });
it('resolves expression with default item when input node is not parent, while still pairing items', () => {
cy.fixture('Test_workflow_5.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Set2');
ndv.getters.inputPanel().contains('6 items').should('exist');
ndv.getters
.outputRunSelector()
.find('input')
.should('exist')
.should('have.value', '2 of 2 (6 items)');
ndv.actions.switchInputMode('Table');
ndv.actions.switchOutputMode('Table');
ndv.getters.backToCanvas().realHover(); // reset to default hover
ndv.getters.inputTableRow(1).should('have.text', '1111');
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.inputTableRow(1).realHover();
cy.wait(100);
ndv.getters.outputHoveringItem().should('not.exist');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1111');
ndv.actions.selectInputNode('Code1');
ndv.getters.inputTableRow(1).realHover();
ndv.getters.inputTableRow(1).should('have.text', '1000');
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.outputTableRow(1).should('have.text', '1000');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
ndv.actions.selectInputNode('Code');
ndv.getters.inputTableRow(1).realHover();
cy.wait(100);
ndv.getters.inputTableRow(1).should('have.text', '6666');
ndv.getters.inputTableRow(1).invoke('attr', 'data-test-id').should('equal', 'hovering-item');
ndv.getters.outputHoveringItem().should('not.exist');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
ndv.actions.selectInputNode('When clicking');
ndv.getters.inputTableRow(1).realHover();
ndv.getters
.inputTableRow(1)
.should('have.text', "This is an item, but it's empty.")
.realHover();
ndv.getters.outputHoveringItem().should('have.length', 6);
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
});
it('can pair items between input and output across branches and runs', () => { it('can pair items between input and output across branches and runs', () => {
cy.fixture('Test_workflow_5.json').then((data) => { cy.fixture('Test_workflow_5.json').then((data) => {
cy.get('body').paste(JSON.stringify(data)); cy.get('body').paste(JSON.stringify(data));
@ -285,7 +227,7 @@ describe('NDV', () => {
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
/* prettier-ignore */ // biome-ignore format:
const PINNED_DATA = [ const PINNED_DATA = [
{ {
"id": "abc", "id": "abc",
@ -321,7 +263,6 @@ describe('NDV', () => {
] ]
} }
]; ];
/* prettier-ignore */
workflowPage.actions.openNode('Get thread details1'); workflowPage.actions.openNode('Get thread details1');
ndv.actions.pastePinnedData(PINNED_DATA); ndv.actions.pastePinnedData(PINNED_DATA);
ndv.actions.close(); ndv.actions.close();

View file

@ -1,7 +1,5 @@
import type { Interception } from 'cypress/types/net-stubbing';
import { META_KEY } from '../constants'; import { META_KEY } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { getPopper } from '../utils';
const workflowPage = new WorkflowPageClass(); const workflowPage = new WorkflowPageClass();
@ -82,32 +80,6 @@ describe('Canvas Actions', () => {
workflowPage.getters.stickies().should('have.length', 0); workflowPage.getters.stickies().should('have.length', 0);
}); });
it('change sticky color', () => {
workflowPage.actions.addSticky();
workflowPage.getters.stickies().should('have.length', 1);
workflowPage.actions.toggleColorPalette();
getPopper().should('be.visible');
workflowPage.actions.pickColor();
workflowPage.actions.toggleColorPalette();
getPopper().should('not.be.visible');
workflowPage.actions.saveWorkflowOnButtonClick();
cy.wait('@createWorkflow').then((interception: Interception) => {
const { request } = interception;
const color = request.body?.nodes[0]?.parameters?.color;
expect(color).to.equal(2);
});
workflowPage.getters.stickies().should('have.length', 1);
});
it('edits sticky and updates content as markdown', () => { it('edits sticky and updates content as markdown', () => {
workflowPage.actions.addSticky(); workflowPage.actions.addSticky();

View file

@ -37,6 +37,16 @@ describe('Resource Locator', () => {
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE); ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);
}); });
it('should show create credentials modal when clicking "add your credential"', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
ndv.getters.resourceLocator('documentId').should('be.visible');
ndv.getters.resourceLocatorInput('documentId').click();
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);
ndv.getters.resourceLocatorAddCredentials().click();
credentialsModal.getters.credentialsEditModal().should('be.visible');
});
it('should show appropriate error when credentials are not valid', () => { it('should show appropriate error when credentials are not valid', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual'); workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet'); workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
@ -55,7 +65,7 @@ describe('Resource Locator', () => {
}); });
it('should show appropriate errors when search filter is required', () => { it('should show appropriate errors when search filter is required', () => {
workflowPage.actions.addNodeToCanvas('Github', true, true, 'On Pull Request'); workflowPage.actions.addNodeToCanvas('Github', true, true, 'On pull request');
ndv.getters.resourceLocator('owner').should('be.visible'); ndv.getters.resourceLocator('owner').should('be.visible');
ndv.getters.resourceLocatorInput('owner').click(); ndv.getters.resourceLocatorInput('owner').click();
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE); ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);

View file

@ -1,3 +1,4 @@
import planData from '../fixtures/Plan_data_opt_in_trial.json';
import { import {
BannerStack, BannerStack,
MainSidebar, MainSidebar,
@ -5,7 +6,6 @@ import {
visitPublicApiPage, visitPublicApiPage,
getPublicApiUpgradeCTA, getPublicApiUpgradeCTA,
} from '../pages'; } from '../pages';
import planData from '../fixtures/Plan_data_opt_in_trial.json';
const mainSidebar = new MainSidebar(); const mainSidebar = new MainSidebar();
const bannerStack = new BannerStack(); const bannerStack = new BannerStack();

View file

@ -1,9 +1,10 @@
import generateOTPToken from 'cypress-otp'; import generateOTPToken from 'cypress-otp';
import { MainSidebar } from './../pages/sidebar/main-sidebar';
import { INSTANCE_OWNER, INSTANCE_ADMIN, BACKEND_BASE_URL } from '../constants'; import { INSTANCE_OWNER, INSTANCE_ADMIN, BACKEND_BASE_URL } from '../constants';
import { SigninPage } from '../pages'; import { SigninPage } from '../pages';
import { PersonalSettingsPage } from '../pages/settings-personal';
import { MfaLoginPage } from '../pages/mfa-login'; import { MfaLoginPage } from '../pages/mfa-login';
import { MainSidebar } from './../pages/sidebar/main-sidebar'; import { PersonalSettingsPage } from '../pages/settings-personal';
const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD'; const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD';

View file

@ -18,7 +18,7 @@ describe('Debug', () => {
it('should be able to debug executions', () => { it('should be able to debug executions', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution'); cy.intercept('GET', '/rest/executions/*').as('getExecution');
cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun'); cy.intercept('POST', '/rest/workflows/**/run?**').as('postWorkflowRun');
cy.signinAsOwner(); cy.signinAsOwner();
@ -117,7 +117,8 @@ describe('Debug', () => {
workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty'); workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty');
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();
ndv.getters.pinDataButton().click(); ndv.actions.unPinData();
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.saveWorkflowUsingKeyboardShortcut(); workflowPage.actions.saveWorkflowUsingKeyboardShortcut();

View file

@ -1,9 +1,9 @@
import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json';
import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json';
import { MainSidebar } from '../pages/sidebar/main-sidebar';
import { TemplatesPage } from '../pages/templates'; import { TemplatesPage } from '../pages/templates';
import { WorkflowPage } from '../pages/workflow'; import { WorkflowPage } from '../pages/workflow';
import { WorkflowsPage } from '../pages/workflows'; import { WorkflowsPage } from '../pages/workflows';
import { MainSidebar } from '../pages/sidebar/main-sidebar';
import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json';
import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json';
const templatesPage = new TemplatesPage(); const templatesPage = new TemplatesPage();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();

View file

@ -128,11 +128,6 @@ describe('Editor actions should work', () => {
createNewWorkflowAndActivate(); createNewWorkflowAndActivate();
}); });
it('after saving a new workflow', () => {
editWorkflowAndDeactivate();
editWorkflowMoreAndActivate();
});
it('after switching between Editor and Executions', () => { it('after switching between Editor and Executions', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
@ -147,7 +142,7 @@ describe('Editor actions should work', () => {
it('after switching between Editor and Debug', () => { it('after switching between Editor and Debug', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution'); cy.intercept('GET', '/rest/executions/*').as('getExecution');
cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun'); cy.intercept('POST', '/rest/workflows/**/run?**').as('postWorkflowRun');
editWorkflowAndDeactivate(); editWorkflowAndDeactivate();
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();

View file

@ -1,34 +1,3 @@
import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils';
import {
addLanguageModelNodeToParent,
addMemoryNodeToParent,
addNodeToCanvas,
addOutputParserNodeToParent,
addToolNodeToParent,
clickExecuteWorkflowButton,
clickManualChatButton,
disableNode,
getExecuteWorkflowButton,
navigateToNewWorkflowPage,
openNode,
} from '../composables/workflow';
import {
clickCreateNewCredential,
clickExecuteNode,
clickGetBackToCanvas,
toggleParameterCheckboxInputByName,
} from '../composables/ndv';
import { setCredentialValues } from '../composables/modals/credential-modal';
import {
closeManualChatModal,
getManualChatDialog,
getManualChatMessages,
getManualChatModal,
getManualChatModalLogs,
getManualChatModalLogsEntries,
getManualChatModalLogsTree,
sendManualChatMessage,
} from '../composables/modals/chat-modal';
import { import {
AGENT_NODE_NAME, AGENT_NODE_NAME,
MANUAL_CHAT_TRIGGER_NODE_NAME, MANUAL_CHAT_TRIGGER_NODE_NAME,
@ -41,7 +10,41 @@ import {
AI_TOOL_WIKIPEDIA_NODE_NAME, AI_TOOL_WIKIPEDIA_NODE_NAME,
BASIC_LLM_CHAIN_NODE_NAME, BASIC_LLM_CHAIN_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME,
CHAT_TRIGGER_NODE_DISPLAY_NAME,
} from './../constants'; } from './../constants';
import {
closeManualChatModal,
getManualChatDialog,
getManualChatMessages,
getManualChatModal,
getManualChatModalLogs,
getManualChatModalLogsEntries,
getManualChatModalLogsTree,
sendManualChatMessage,
} from '../composables/modals/chat-modal';
import { setCredentialValues } from '../composables/modals/credential-modal';
import {
clickCreateNewCredential,
clickExecuteNode,
clickGetBackToCanvas,
toggleParameterCheckboxInputByName,
} from '../composables/ndv';
import {
addLanguageModelNodeToParent,
addMemoryNodeToParent,
addNodeToCanvas,
addOutputParserNodeToParent,
addToolNodeToParent,
clickExecuteWorkflowButton,
clickManualChatButton,
disableNode,
getExecuteWorkflowButton,
navigateToNewWorkflowPage,
getNodes,
openNode,
getConnectionBySourceAndTarget,
} from '../composables/workflow';
import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils';
describe('Langchain Integration', () => { describe('Langchain Integration', () => {
beforeEach(() => { beforeEach(() => {
@ -331,4 +334,27 @@ describe('Langchain Integration', () => {
closeManualChatModal(); closeManualChatModal();
}); });
it('should auto-add chat trigger and basic LLM chain when adding LLM node', () => {
addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true);
getConnectionBySourceAndTarget(
CHAT_TRIGGER_NODE_DISPLAY_NAME,
BASIC_LLM_CHAIN_NODE_NAME,
).should('exist');
getConnectionBySourceAndTarget(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
BASIC_LLM_CHAIN_NODE_NAME,
).should('exist');
getNodes().should('have.length', 3);
});
it('should not auto-add nodes if AI nodes are already present', () => {
addNodeToCanvas(AGENT_NODE_NAME, true);
addNodeToCanvas(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, true);
getConnectionBySourceAndTarget(CHAT_TRIGGER_NODE_DISPLAY_NAME, AGENT_NODE_NAME).should('exist');
getNodes().should('have.length', 3);
});
}); });

View file

@ -1,23 +1,32 @@
import workflow from '../fixtures/Manual_wait_set.json'; import workflow from '../fixtures/Manual_wait_set.json';
import { importWorkflow, vistDemoPage } from '../pages/demo'; import { importWorkflow, visitDemoPage } from '../pages/demo';
import { errorToast } from '../pages/notifications';
import { WorkflowPage } from '../pages/workflow'; import { WorkflowPage } from '../pages/workflow';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
describe('Demo', () => { describe('Demo', () => {
beforeEach(() => {
cy.overrideSettings({ previewMode: true });
cy.signout();
});
it('can import template', () => { it('can import template', () => {
vistDemoPage(); visitDemoPage();
errorToast().should('not.exist');
importWorkflow(workflow); importWorkflow(workflow);
workflowPage.getters.canvasNodes().should('have.length', 3); workflowPage.getters.canvasNodes().should('have.length', 3);
}); });
it('can override theme to dark', () => { it('can override theme to dark', () => {
vistDemoPage('dark'); visitDemoPage('dark');
cy.get('body').should('have.attr', 'data-theme', 'dark'); cy.get('body').should('have.attr', 'data-theme', 'dark');
errorToast().should('not.exist');
}); });
it('can override theme to light', () => { it('can override theme to light', () => {
vistDemoPage('light'); visitDemoPage('light');
cy.get('body').should('have.attr', 'data-theme', 'light'); cy.get('body').should('have.attr', 'data-theme', 'light');
errorToast().should('not.exist');
}); });
}); });

View file

@ -35,13 +35,14 @@ describe('Personal Settings', () => {
successToast().find('.el-notification__closeBtn').click(); successToast().find('.el-notification__closeBtn').click();
}); });
}); });
// eslint-disable-next-line n8n-local-rules/no-skipped-tests
it('not allow malicious values for personal data', () => { it('not allow malicious values for personal data', () => {
cy.visit('/settings/personal'); cy.visit('/settings/personal');
INVALID_NAMES.forEach((name) => { INVALID_NAMES.forEach((name) => {
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name); cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name);
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name); cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name);
cy.getByTestId('save-settings-button').click(); cy.getByTestId('save-settings-button').click();
errorToast().should('contain', 'Malicious firstName | Malicious lastName'); errorToast().should('contain', 'Potentially malicious string');
errorToast().find('.el-notification__closeBtn').click(); errorToast().find('.el-notification__closeBtn').click();
}); });
}); });

View file

@ -1,3 +1,8 @@
import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal';
import * as formStep from '../composables/setup-template-form-step';
import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button';
import TestTemplate1 from '../fixtures/Test_Template_1.json';
import TestTemplate2 from '../fixtures/Test_Template_2.json';
import { import {
clickUseWorkflowButtonByTitle, clickUseWorkflowButtonByTitle,
visitTemplateCollectionPage, visitTemplateCollectionPage,
@ -5,11 +10,6 @@ import {
} from '../pages/template-collection'; } from '../pages/template-collection';
import * as templateCredentialsSetupPage from '../pages/template-credential-setup'; import * as templateCredentialsSetupPage from '../pages/template-credential-setup';
import { WorkflowPage } from '../pages/workflow'; import { WorkflowPage } from '../pages/workflow';
import * as formStep from '../composables/setup-template-form-step';
import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button';
import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal';
import TestTemplate1 from '../fixtures/Test_Template_1.json';
import TestTemplate2 from '../fixtures/Test_Template_2.json';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();

View file

@ -1,10 +1,10 @@
import { WorkflowsPage } from '../pages/workflows';
import { import {
closeVersionUpdatesPanel, closeVersionUpdatesPanel,
getVersionCard, getVersionCard,
getVersionUpdatesPanelOpenButton, getVersionUpdatesPanelOpenButton,
openVersionUpdatesPanel, openVersionUpdatesPanel,
} from '../composables/versions'; } from '../composables/versions';
import { WorkflowsPage } from '../pages/workflows';
const workflowsPage = new WorkflowsPage(); const workflowsPage = new WorkflowsPage();

View file

@ -1,4 +1,6 @@
import * as projects from '../composables/projects';
import { import {
INSTANCE_ADMIN,
INSTANCE_MEMBERS, INSTANCE_MEMBERS,
INSTANCE_OWNER, INSTANCE_OWNER,
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
@ -11,9 +13,10 @@ import {
CredentialsPage, CredentialsPage,
WorkflowExecutionsTab, WorkflowExecutionsTab,
NDV, NDV,
MainSidebar,
} from '../pages'; } from '../pages';
import * as projects from '../composables/projects'; import { clearNotifications } from '../pages/notifications';
import { getVisibleSelect } from '../utils'; import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils';
const workflowsPage = new WorkflowsPage(); const workflowsPage = new WorkflowsPage();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
@ -21,6 +24,7 @@ const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal(); const credentialsModal = new CredentialsModal();
const executionsTab = new WorkflowExecutionsTab(); const executionsTab = new WorkflowExecutionsTab();
const ndv = new NDV(); const ndv = new NDV();
const mainSidebar = new MainSidebar();
describe('Projects', { disableAutoLogin: true }, () => { describe('Projects', { disableAutoLogin: true }, () => {
before(() => { before(() => {
@ -237,10 +241,30 @@ describe('Projects', { disableAutoLogin: true }, () => {
cy.signinAsMember(1); cy.signinAsMember(1);
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
projects.getAddProjectButton().should('not.exist'); cy.getByTestId('add-project-menu-item').should('not.exist');
projects.getMenuItems().should('not.exist'); projects.getMenuItems().should('not.exist');
}); });
it('should not show viewer role if not licensed', () => {
cy.signinAsOwner();
cy.visit(workflowsPage.url);
projects.getMenuItems().first().click();
projects.getProjectTabSettings().click();
cy.get(
`[data-test-id="user-list-item-${INSTANCE_MEMBERS[0].email}"] [data-test-id="projects-settings-user-role-select"]`,
).click();
cy.get('.el-select-dropdown__item.is-disabled')
.should('contain.text', 'Viewer')
.get('span:contains("Upgrade")')
.filter(':visible')
.click();
getVisibleModalOverlay().should('contain.text', 'Upgrade to unlock additional roles');
});
describe('when starting from scratch', () => { describe('when starting from scratch', () => {
beforeEach(() => { beforeEach(() => {
cy.resetDatabase(); cy.resetDatabase();
@ -257,7 +281,7 @@ describe('Projects', { disableAutoLogin: true }, () => {
// Create a project and add a credential to it // Create a project and add a credential to it
cy.intercept('POST', '/rest/projects').as('projectCreate'); cy.intercept('POST', '/rest/projects').as('projectCreate');
projects.getAddProjectButton().should('contain', 'Add project').should('be.visible').click(); projects.getAddProjectButton().click();
cy.wait('@projectCreate'); cy.wait('@projectCreate');
projects.getMenuItems().should('have.length', 1); projects.getMenuItems().should('have.length', 1);
projects.getMenuItems().first().click(); projects.getMenuItems().first().click();
@ -418,45 +442,55 @@ describe('Projects', { disableAutoLogin: true }, () => {
}); });
it('should move resources between projects', () => { it('should move resources between projects', () => {
cy.signin(INSTANCE_OWNER); cy.signinAsOwner();
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
// Create a workflow and a credential in the Home project // Create a workflow and a credential in the Home project
workflowsPage.getters.workflowCards().should('not.have.length'); workflowsPage.getters.workflowCards().should('not.have.length');
workflowsPage.getters.newWorkflowButtonCard().click(); workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Home project'); projects.createWorkflow('Test_workflow_1.json', 'Workflow in Home project');
clearNotifications();
projects.getHomeButton().click(); projects.getHomeButton().click();
projects.getProjectTabCredentials().should('be.visible').click(); projects.getProjectTabCredentials().should('be.visible').click();
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Home project'); projects.createCredential('Credential in Home project');
clearNotifications();
// Create a project and add a credential and a workflow to it // Create a project and add a credential and a workflow to it
projects.createProject('Project 1'); projects.createProject('Project 1');
clearNotifications();
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Project 1'); projects.createCredential('Credential in Project 1');
clearNotifications();
projects.getProjectTabWorkflows().click(); projects.getProjectTabWorkflows().click();
workflowsPage.getters.newWorkflowButtonCard().click(); workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 1'); projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 1');
clearNotifications();
// Create another project and add a credential and a workflow to it // Create another project and add a credential and a workflow to it
projects.createProject('Project 2'); projects.createProject('Project 2');
clearNotifications();
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click(); credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Project 2'); projects.createCredential('Credential in Project 2');
clearNotifications();
projects.getProjectTabWorkflows().click(); projects.getProjectTabWorkflows().click();
workflowsPage.getters.newWorkflowButtonCard().click(); workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 2'); projects.createWorkflow('Test_workflow_1.json', 'Workflow in Project 2');
clearNotifications();
// Move the workflow owned by me from Home to Project 1 // Move the workflow Personal from Home to Project 1
projects.getHomeButton().click(); projects.getHomeButton().click();
workflowsPage.getters workflowsPage.getters
.workflowCards() .workflowCards()
.should('have.length', 3) .should('have.length', 3)
.filter(':contains("Owned by me")') .filter(':contains("Personal")')
.should('exist'); .should('exist');
workflowsPage.getters.workflowCardActions('Workflow in Home project').click(); workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
workflowsPage.getters.workflowMoveButton().click(); workflowsPage.getters.workflowMoveButton().click();
@ -464,54 +498,96 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects projects
.getResourceMoveModal() .getResourceMoveModal()
.should('be.visible') .should('be.visible')
.find('button:contains("Next")') .find('button:contains("Move workflow")')
.should('be.disabled'); .should('be.disabled');
projects.getProjectMoveSelect().click(); projects.getProjectMoveSelect().click();
getVisibleSelect() getVisibleSelect()
.find('li') .find('li')
.should('have.length', 2) .should('have.length', 5)
.first() .filter(':contains("Project 1")')
.should('contain.text', 'Project 1')
.click();
projects.getResourceMoveModal().find('button:contains("Next")').click();
projects
.getResourceMoveConfirmModal()
.should('be.visible')
.find('button:contains("Confirm")')
.should('be.disabled');
projects
.getResourceMoveConfirmModal()
.find('input[type="checkbox"]')
.first()
.parents('label')
.click();
projects
.getResourceMoveConfirmModal()
.find('button:contains("Confirm")')
.should('be.disabled');
projects
.getResourceMoveConfirmModal()
.find('input[type="checkbox"]')
.last()
.parents('label')
.click();
projects
.getResourceMoveConfirmModal()
.find('button:contains("Confirm")')
.should('not.be.disabled')
.click(); .click();
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
clearNotifications();
workflowsPage.getters workflowsPage.getters
.workflowCards() .workflowCards()
.should('have.length', 3) .should('have.length', 3)
.filter(':contains("Owned by me")') .filter(':contains("Personal")')
.should('not.exist'); .should('not.exist');
// Move the workflow from Project 1 to Project 2
projects.getMenuItems().first().click();
workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
workflowsPage.getters.workflowMoveButton().click();
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move workflow")')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 5)
.filter(':contains("Project 2")')
.click();
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
// Move the workflow from Project 2 to a member user
projects.getMenuItems().last().click();
workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
workflowsPage.getters.workflowMoveButton().click();
clearNotifications();
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move workflow")')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 5)
.filter(`:contains("${INSTANCE_MEMBERS[0].email}")`)
.click();
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
workflowsPage.getters.workflowCards().should('have.length', 1);
// Move the workflow from member user back to Home
projects.getHomeButton().click();
workflowsPage.getters
.workflowCards()
.should('have.length', 3)
.filter(':has(.n8n-badge:contains("Project"))')
.should('have.length', 2);
workflowsPage.getters.workflowCardActions('Workflow in Home project').click();
workflowsPage.getters.workflowMoveButton().click();
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move workflow")')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 5)
.filter(`:contains("${INSTANCE_OWNER.email}")`)
.click();
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
clearNotifications();
workflowsPage.getters
.workflowCards()
.should('have.length', 3)
.filter(':contains("Personal")')
.should('have.length', 1);
// Move the credential from Project 1 to Project 2 // Move the credential from Project 1 to Project 2
projects.getMenuItems().first().click(); projects.getMenuItems().first().click();
workflowsPage.getters.workflowCards().should('have.length', 2);
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 1); credentialsPage.getters.credentialCards().should('have.length', 1);
credentialsPage.getters.credentialCardActions('Credential in Project 1').click(); credentialsPage.getters.credentialCardActions('Credential in Project 1').click();
@ -520,48 +596,239 @@ describe('Projects', { disableAutoLogin: true }, () => {
projects projects
.getResourceMoveModal() .getResourceMoveModal()
.should('be.visible') .should('be.visible')
.find('button:contains("Next")') .find('button:contains("Move credential")')
.should('be.disabled'); .should('be.disabled');
projects.getProjectMoveSelect().click(); projects.getProjectMoveSelect().click();
getVisibleSelect() getVisibleSelect()
.find('li') .find('li')
.should('have.length', 1) .should('have.length', 5)
.first() .filter(':contains("Project 2")')
.should('contain.text', 'Project 2')
.click();
projects.getResourceMoveModal().find('button:contains("Next")').click();
projects
.getResourceMoveConfirmModal()
.should('be.visible')
.find('button:contains("Confirm")')
.should('be.disabled');
projects
.getResourceMoveConfirmModal()
.find('input[type="checkbox"]')
.first()
.parents('label')
.click();
projects
.getResourceMoveConfirmModal()
.find('button:contains("Confirm")')
.should('be.disabled');
projects
.getResourceMoveConfirmModal()
.find('input[type="checkbox"]')
.last()
.parents('label')
.click();
projects
.getResourceMoveConfirmModal()
.find('button:contains("Confirm")')
.should('not.be.disabled')
.click(); .click();
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
clearNotifications();
credentialsPage.getters.credentialCards().should('not.have.length'); credentialsPage.getters.credentialCards().should('not.have.length');
// Move the credential from Project 2 to admin user
projects.getMenuItems().last().click(); projects.getMenuItems().last().click();
projects.getProjectTabCredentials().click(); projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 2); credentialsPage.getters.credentialCards().should('have.length', 2);
credentialsPage.getters.credentialCardActions('Credential in Project 1').click();
credentialsPage.getters.credentialMoveButton().click();
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move credential")')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 5)
.filter(`:contains("${INSTANCE_ADMIN.email}")`)
.click();
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
credentialsPage.getters.credentialCards().should('have.length', 1);
// Move the credential from admin user back to instance owner
projects.getHomeButton().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().should('have.length', 3);
credentialsPage.getters.credentialCardActions('Credential in Project 1').click();
credentialsPage.getters.credentialMoveButton().click();
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move credential")')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 5)
.filter(`:contains("${INSTANCE_OWNER.email}")`)
.click();
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
clearNotifications();
credentialsPage.getters
.credentialCards()
.should('have.length', 3)
.filter(':contains("Personal")')
.should('have.length', 2);
// Move the credential from admin user back to its original project (Project 1)
credentialsPage.getters.credentialCardActions('Credential in Project 1').click();
credentialsPage.getters.credentialMoveButton().click();
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move credential")')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 5)
.filter(':contains("Project 1")')
.click();
projects.getResourceMoveModal().find('button:contains("Move credential")').click();
projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters
.credentialCards()
.filter(':contains("Credential in Project 1")')
.should('have.length', 1);
});
it('should allow to change inaccessible credential when the workflow was moved to a team project', () => {
cy.signinAsOwner();
cy.visit(workflowsPage.url);
// Create a credential in the Home project
projects.getProjectTabCredentials().should('be.visible').click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Credential in Home project');
// Create a workflow in the Home project
projects.getHomeButton().click();
workflowsPage.getters.workflowCards().should('not.have.length');
workflowsPage.getters.newWorkflowButtonCard().click();
workflowsPage.getters.workflowCards().should('not.have.length');
workflowsPage.getters.newWorkflowButtonCard().click();
workflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
workflowPage.actions.addNodeToCanvas(NOTION_NODE_NAME, true, true);
ndv.getters.backToCanvas().click();
workflowPage.actions.saveWorkflowOnButtonClick();
// Create a project and add a user to it
projects.createProject('Project 1');
projects.addProjectMember(INSTANCE_MEMBERS[0].email);
projects.getProjectSettingsSaveButton().click();
// Move the workflow from Home to Project 1
projects.getHomeButton().click();
workflowsPage.getters
.workflowCards()
.should('have.length', 1)
.filter(':contains("Personal")')
.should('exist');
workflowsPage.getters.workflowCardActions('My workflow').click();
workflowsPage.getters.workflowMoveButton().click();
projects
.getResourceMoveModal()
.should('be.visible')
.find('button:contains("Move workflow")')
.should('be.disabled');
projects.getProjectMoveSelect().click();
getVisibleSelect()
.find('li')
.should('have.length', 4)
.filter(':contains("Project 1")')
.click();
projects.getResourceMoveModal().find('button:contains("Move workflow")').click();
workflowsPage.getters
.workflowCards()
.should('have.length', 1)
.filter(':contains("Personal")')
.should('not.exist');
//Log out with instance owner and log in with the member user
mainSidebar.actions.openUserMenu();
cy.getByTestId('user-menu-item-logout').click();
cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
cy.getByTestId('form-submit-button').click();
// Open the moved workflow
workflowsPage.getters.workflowCards().should('have.length', 1);
workflowsPage.getters.workflowCards().first().click();
// Check if the credential can be changed
workflowPage.getters.canvasNodeByName(NOTION_NODE_NAME).should('be.visible').dblclick();
ndv.getters.credentialInput().find('input').should('be.enabled');
});
it('should handle viewer role', () => {
cy.enableFeature('projectRole:viewer');
cy.signinAsOwner();
cy.visit(workflowsPage.url);
projects.createProject('Development');
projects.addProjectMember(INSTANCE_MEMBERS[0].email, 'Viewer');
projects.getProjectSettingsSaveButton().click();
projects.getProjectTabWorkflows().click();
workflowsPage.getters.newWorkflowButtonCard().click();
projects.createWorkflow('Test_workflow_4_executions_view.json', 'WF with random error');
executionsTab.actions.createManualExecutions(2);
executionsTab.actions.toggleNodeEnabled('Error');
executionsTab.actions.createManualExecutions(2);
workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.emptyListCreateCredentialButton().click();
projects.createCredential('Notion API');
mainSidebar.actions.openUserMenu();
cy.getByTestId('user-menu-item-logout').click();
cy.get('input[name="email"]').type(INSTANCE_MEMBERS[0].email);
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
cy.getByTestId('form-submit-button').click();
mainSidebar.getters.executions().click();
cy.getByTestId('global-execution-list-item').first().find('td:last button').click();
getVisibleDropdown()
.find('li')
.filter(':contains("Retry")')
.should('have.class', 'is-disabled');
getVisibleDropdown()
.find('li')
.filter(':contains("Delete")')
.should('have.class', 'is-disabled');
projects.getMenuItems().first().click();
cy.getByTestId('workflow-card-name').should('be.visible').first().click();
workflowPage.getters.nodeViewRoot().should('be.visible');
workflowPage.getters.executeWorkflowButton().should('not.exist');
workflowPage.getters.nodeCreatorPlusButton().should('not.exist');
workflowPage.getters.canvasNodes().should('have.length', 3).last().click();
cy.get('body').type('{backspace}');
workflowPage.getters.canvasNodes().should('have.length', 3).last().rightclick();
getVisibleDropdown()
.find('li')
.should('be.visible')
.filter(
':contains("Open"), :contains("Copy"), :contains("Select all"), :contains("Clear selection")',
)
.should('not.have.class', 'is-disabled');
cy.get('body').type('{esc}');
executionsTab.actions.switchToExecutionsTab();
cy.getByTestId('retry-execution-button')
.should('be.visible')
.find('.is-disabled')
.should('exist');
cy.get('button:contains("Debug")').should('be.disabled');
cy.get('button[title="Retry execution"]').should('be.disabled');
cy.get('button[title="Delete this execution"]').should('be.disabled');
projects.getMenuItems().first().click();
projects.getProjectTabCredentials().click();
credentialsPage.getters.credentialCards().filter(':contains("Notion")').click();
cy.getByTestId('node-credentials-config-container')
.should('be.visible')
.find('input')
.should('not.have.length');
}); });
}); });
}); });

View file

@ -1,8 +1,14 @@
import { NodeCreator } from '../pages/features/node-creator'; import {
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; addNodeToCanvas,
import { NDV } from '../pages/ndv'; addRetrieverNodeToParent,
import { getVisibleSelect } from '../utils'; addVectorStoreNodeToParent,
getNodeCreatorItems,
} from '../composables/workflow';
import { IF_NODE_NAME } from '../constants'; import { IF_NODE_NAME } from '../constants';
import { NodeCreator } from '../pages/features/node-creator';
import { NDV } from '../pages/ndv';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { getVisibleSelect } from '../utils';
const nodeCreatorFeature = new NodeCreator(); const nodeCreatorFeature = new NodeCreator();
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
@ -504,4 +510,38 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.searchBar().find('input').clear().type('gith'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('gith');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'GitHub'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'GitHub');
}); });
it('should show vector stores actions', () => {
const actions = [
'Get ranked documents from vector store',
'Add documents to vector store',
'Retrieve documents for AI processing',
];
nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('Vector Store');
getNodeCreatorItems().then((items) => {
const vectorStores = items.map((_i, el) => el.innerText);
// Loop over all vector stores and check if they have the three actions
vectorStores.each((_i, vectorStore) => {
nodeCreatorFeature.getters.getCreatorItem(vectorStore).click();
actions.forEach((action) => {
nodeCreatorFeature.getters.getCreatorItem(action).should('be.visible');
});
cy.realPress('ArrowLeft');
});
});
});
it('should add node directly for sub-connection', () => {
addNodeToCanvas('Question and Answer Chain', true);
addRetrieverNodeToParent('Vector Store Retriever', 'Question and Answer Chain');
cy.realPress('Escape');
addVectorStoreNodeToParent('In-Memory Vector Store', 'Vector Store Retriever');
cy.realPress('Escape');
WorkflowPage.getters.canvasNodes().should('have.length', 4);
});
}); });

View file

@ -23,6 +23,7 @@ describe('Manual partial execution', () => {
canvas.actions.openNode('Webhook1'); canvas.actions.openNode('Webhook1');
ndv.getters.nodeRunSuccessIndicator().should('exist'); ndv.getters.nodeRunSuccessIndicator().should('exist');
ndv.getters.nodeRunTooltipIndicator().should('exist');
ndv.getters.outputRunSelector().should('not.exist'); // single run ndv.getters.outputRunSelector().should('not.exist'); // single run
}); });
}); });

View file

@ -0,0 +1,26 @@
import { getSaveChangesModal } from '../composables/modals/save-changes-modal';
import { EDIT_FIELDS_SET_NODE_NAME } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
const WorkflowsPage = new WorkflowsPageClass();
const WorkflowPage = new WorkflowPageClass();
describe('Workflows', () => {
beforeEach(() => {
cy.visit(WorkflowsPage.url);
});
it('should ask to save unsaved changes before leaving route', () => {
WorkflowsPage.getters.newWorkflowButtonCard().should('be.visible');
WorkflowsPage.getters.newWorkflowButtonCard().click();
cy.createFixtureWorkflow('Test_workflow_1.json', 'Empty State Card Workflow');
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
cy.getByTestId('project-home-menu-item').click();
getSaveChangesModal().should('be.visible');
});
});

View file

@ -0,0 +1,528 @@
import { type ICredentialType } from 'n8n-workflow';
import { clickCreateNewCredential, openCredentialSelect } from '../composables/ndv';
import { GMAIL_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
import { CredentialsModal, CredentialsPage, NDV, WorkflowPage } from '../pages';
import { AIAssistant } from '../pages/features/ai-assistant';
import { getVisibleSelect } from '../utils';
const wf = new WorkflowPage();
const ndv = new NDV();
const aiAssistant = new AIAssistant();
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
describe('AI Assistant::disabled', () => {
beforeEach(() => {
aiAssistant.actions.disableAssistant();
wf.actions.visit();
});
it('does not show assistant button if feature is disabled', () => {
aiAssistant.getters.askAssistantFloatingButton().should('not.exist');
});
});
describe('AI Assistant::enabled', () => {
beforeEach(() => {
aiAssistant.actions.enableAssistant();
wf.actions.visit();
});
after(() => {
aiAssistant.actions.disableAssistant();
});
it('renders placeholder UI', () => {
aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.getters.askAssistantChat().should('be.visible');
aiAssistant.getters.placeholderMessage().should('be.visible');
aiAssistant.getters.chatInput().should('be.visible');
aiAssistant.getters.sendMessageButton().should('be.disabled');
aiAssistant.getters.closeChatButton().should('be.visible');
aiAssistant.getters.closeChatButton().click();
aiAssistant.getters.askAssistantChat().should('not.be.visible');
});
it('should resize assistant chat up', () => {
aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
aiAssistant.getters.askAssistantChat().then((element) => {
const { width, left } = element[0].getBoundingClientRect();
cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left - 10, 0], {
abs: true,
clickToFinish: true,
});
aiAssistant.getters.askAssistantChat().then((newElement) => {
const newWidth = newElement[0].getBoundingClientRect().width;
expect(newWidth).to.be.greaterThan(width);
});
});
});
it('should resize assistant chat down', () => {
aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.getters.askAssistantSidebarResizer().should('be.visible');
aiAssistant.getters.askAssistantChat().then((element) => {
const { width, left } = element[0].getBoundingClientRect();
cy.drag(aiAssistant.getters.askAssistantSidebarResizer(), [left + 10, 0], {
abs: true,
clickToFinish: true,
});
aiAssistant.getters.askAssistantChat().then((newElement) => {
const newWidth = newElement[0].getBoundingClientRect().width;
expect(newWidth).to.be.lessThan(width);
});
});
});
it('should start chat session from node error view', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesAll().should('have.length', 1);
aiAssistant.getters
.chatMessagesAll()
.eq(0)
.should('contain.text', 'Hey, this is an assistant message');
aiAssistant.getters.nodeErrorViewAssistantButton().should('be.disabled');
});
it('should render chat input correctly', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest');
// Send button should be disabled when input is empty
aiAssistant.getters.sendMessageButton().should('be.disabled');
aiAssistant.getters.chatInput().type('Yo ');
aiAssistant.getters.sendMessageButton().should('not.be.disabled');
aiAssistant.getters.chatInput().then((element) => {
const { height } = element[0].getBoundingClientRect();
// Shift + Enter should add a new line
aiAssistant.getters.chatInput().type('Hello{shift+enter}there');
aiAssistant.getters.chatInput().then((newElement) => {
const newHeight = newElement[0].getBoundingClientRect().height;
// Chat input should grow as user adds new lines
expect(newHeight).to.be.greaterThan(height);
aiAssistant.getters.sendMessageButton().click();
cy.wait('@chatRequest');
// New lines should be rendered as <br> in the chat
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
aiAssistant.getters.chatMessagesUser().eq(0).find('br').should('have.length', 1);
// Chat input should be cleared now
aiAssistant.getters.chatInput().should('have.value', '');
});
});
});
it('should render and handle quick replies', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/quick_reply_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.quickReplyButtons().should('have.length', 2);
aiAssistant.getters.quickReplyButtons().eq(0).click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', "Sure, let's do it");
});
it('should warn before starting a new session', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Edit Fields');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
cy.wait('@chatRequest');
aiAssistant.getters.closeChatButton().click();
ndv.getters.backToCanvas().click();
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
// Since we already have an active session, a warning should be shown
aiAssistant.getters.newAssistantSessionModal().should('be.visible');
aiAssistant.getters
.newAssistantSessionModal()
.find('button')
.contains('Start new session')
.click();
cy.wait('@chatRequest');
// New session should start with initial assistant message
aiAssistant.getters.chatMessagesAll().should('have.length', 1);
});
it('should apply code diff to code node', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/code_diff_suggestion_response.json',
}).as('chatRequest');
cy.intercept('POST', '/rest/ai/chat/apply-suggestion', {
statusCode: 200,
fixture: 'aiAssistant/responses/apply_code_diff_response.json',
}).as('applySuggestion');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Code');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click({ force: true });
cy.wait('@chatRequest');
// Should have two assistant messages
aiAssistant.getters.chatMessagesAll().should('have.length', 2);
aiAssistant.getters.codeDiffs().should('have.length', 1);
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
aiAssistant.getters.applyCodeDiffButtons().first().click();
cy.wait('@applySuggestion');
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 0);
aiAssistant.getters.undoReplaceCodeButtons().should('have.length', 1);
aiAssistant.getters.codeReplacedMessage().should('be.visible');
ndv.getters
.parameterInput('jsCode')
.get('.cm-content')
.should('contain.text', 'item.json.myNewField = 1');
// Clicking undo should revert the code back but not call the assistant
aiAssistant.getters.undoReplaceCodeButtons().first().click();
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
aiAssistant.getters.codeReplacedMessage().should('not.exist');
cy.get('@applySuggestion.all').then((interceptions) => {
expect(interceptions).to.have.length(1);
});
ndv.getters
.parameterInput('jsCode')
.get('.cm-content')
.should('contain.text', 'item.json.myNewField = 1aaa');
// Replacing the code again should also not call the assistant
cy.get('@applySuggestion.all').then((interceptions) => {
expect(interceptions).to.have.length(1);
});
aiAssistant.getters.applyCodeDiffButtons().should('have.length', 1);
aiAssistant.getters.applyCodeDiffButtons().first().click();
ndv.getters
.parameterInput('jsCode')
.get('.cm-content')
.should('contain.text', 'item.json.myNewField = 1');
});
it('should end chat session when `end_session` event is received', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/end_session_response.json',
}).as('chatRequest');
cy.createFixtureWorkflow('aiAssistant/workflows/test_workflow.json');
wf.actions.openNode('Stop and Error');
ndv.getters.nodeExecuteButton().click();
aiAssistant.getters.nodeErrorViewAssistantButton().click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
});
it('should reset session after it ended and sidebar is closed', () => {
cy.intercept('POST', '/rest/ai/chat', (req) => {
req.reply((res) => {
if (['init-support-chat'].includes(req.body.payload.type)) {
res.send({
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
});
} else {
res.send({ statusCode: 200, fixture: 'aiAssistant/responses/end_session_response.json' });
}
});
}).as('chatRequest');
aiAssistant.actions.openChat();
aiAssistant.actions.sendMessage('Hello');
cy.wait('@chatRequest');
aiAssistant.actions.closeChat();
aiAssistant.actions.openChat();
// After closing and reopening the chat, all messages should be still there
aiAssistant.getters.chatMessagesAll().should('have.length', 2);
// End the session
aiAssistant.actions.sendMessage('Thanks, bye');
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesSystem().should('have.length', 1);
aiAssistant.getters.chatMessagesSystem().first().should('contain.text', 'session has ended');
aiAssistant.actions.closeChat();
aiAssistant.actions.openChat();
// Now, session should be reset
aiAssistant.getters.placeholderMessage().should('be.visible');
});
it('Should not reset assistant session when workflow is saved', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
wf.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
aiAssistant.actions.openChat();
aiAssistant.actions.sendMessage('Hello');
wf.actions.openNode(SCHEDULE_TRIGGER_NODE_NAME);
ndv.getters.nodeExecuteButton().click();
wf.getters.isWorkflowSaved();
aiAssistant.getters.placeholderMessage().should('not.exist');
});
});
describe('AI Assistant Credential Help', () => {
beforeEach(() => {
aiAssistant.actions.enableAssistant();
wf.actions.visit();
});
after(() => {
aiAssistant.actions.disableAssistant();
});
it('should start credential help from node credential', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
wf.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
wf.actions.addNodeToCanvas(GMAIL_NODE_NAME);
wf.actions.openNode('Gmail');
openCredentialSelect();
clickCreateNewCredential();
aiAssistant.getters.credentialEditAssistantButton().find('button').should('be.visible');
aiAssistant.getters.credentialEditAssistantButton().find('button').click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
aiAssistant.getters
.chatMessagesUser()
.eq(0)
.should('contain.text', 'How do I set up the credentials for Gmail OAuth2 API?');
aiAssistant.getters
.chatMessagesAssistant()
.eq(0)
.should('contain.text', 'Hey, this is an assistant message');
aiAssistant.getters.credentialEditAssistantButton().find('button').should('be.disabled');
});
it('should start credential help from credential list', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.getters.newCredentialModal().should('be.visible');
credentialsModal.getters.newCredentialTypeSelect().should('be.visible');
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
credentialsModal.getters.newCredentialTypeButton().click();
aiAssistant.getters.credentialEditAssistantButton().find('button').should('be.visible');
aiAssistant.getters.credentialEditAssistantButton().find('button').click();
cy.wait('@chatRequest');
aiAssistant.getters.chatMessagesUser().should('have.length', 1);
aiAssistant.getters
.chatMessagesUser()
.eq(0)
.should('contain.text', 'How do I set up the credentials for Notion API?');
aiAssistant.getters
.chatMessagesAssistant()
.eq(0)
.should('contain.text', 'Hey, this is an assistant message');
aiAssistant.getters.credentialEditAssistantButton().find('button').should('be.disabled');
});
it('should not show assistant button when click to connect', () => {
cy.intercept('/types/credentials.json', { middleware: true }, (req) => {
req.headers['cache-control'] = 'no-cache, no-store';
req.on('response', (res) => {
const credentials: ICredentialType[] = res.body || [];
const index = credentials.findIndex((c) => c.name === 'slackOAuth2Api');
credentials[index] = {
...credentials[index],
__overwrittenProperties: ['clientId', 'clientSecret'],
};
});
});
wf.actions.visit(true);
wf.actions.addNodeToCanvas('Manual');
wf.actions.addNodeToCanvas('Slack', true, true, 'Get a channel');
wf.getters.nodeCredentialsSelect().should('exist');
wf.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
credentialsModal.getters.credentialAuthTypeRadioButtons().first().click();
ndv.getters.copyInput().should('not.exist');
credentialsModal.getters.oauthConnectButton().should('have.length', 1);
credentialsModal.getters.credentialInputs().should('have.length', 0);
aiAssistant.getters.credentialEditAssistantButton().should('not.exist');
credentialsModal.getters.credentialAuthTypeRadioButtons().eq(1).click();
credentialsModal.getters.credentialInputs().should('have.length', 1);
aiAssistant.getters.credentialEditAssistantButton().should('exist');
});
it('should not show assistant button when click to connect with some fields', () => {
cy.intercept('/types/credentials.json', { middleware: true }, (req) => {
req.headers['cache-control'] = 'no-cache, no-store';
req.on('response', (res) => {
const credentials: ICredentialType[] = res.body || [];
const index = credentials.findIndex((c) => c.name === 'microsoftOutlookOAuth2Api');
credentials[index] = {
...credentials[index],
__overwrittenProperties: ['authUrl', 'accessTokenUrl', 'clientId', 'clientSecret'],
};
});
});
wf.actions.visit(true);
wf.actions.addNodeToCanvas('Manual');
wf.actions.addNodeToCanvas('Microsoft Outlook', true, true, 'Get a calendar');
wf.getters.nodeCredentialsSelect().should('exist');
wf.getters.nodeCredentialsSelect().click();
getVisibleSelect().find('li').last().click();
ndv.getters.copyInput().should('not.exist');
credentialsModal.getters.oauthConnectButton().should('have.length', 1);
credentialsModal.getters.credentialInputs().should('have.length', 1);
aiAssistant.getters.credentialEditAssistantButton().should('not.exist');
});
});
describe('General help', () => {
beforeEach(() => {
aiAssistant.actions.enableAssistant();
wf.actions.visit();
});
it('assistant returns code snippet', () => {
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/code_snippet_response.json',
}).as('chatRequest');
aiAssistant.getters.askAssistantFloatingButton().should('be.visible');
aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.getters.askAssistantChat().should('be.visible');
aiAssistant.getters.placeholderMessage().should('be.visible');
aiAssistant.getters.chatInput().should('be.visible');
aiAssistant.getters.chatInput().type('Show me an expression');
aiAssistant.getters.sendMessageButton().click();
aiAssistant.getters.chatMessagesAll().should('have.length', 3);
aiAssistant.getters.chatMessagesUser().eq(0).should('contain.text', 'Show me an expression');
aiAssistant.getters
.chatMessagesAssistant()
.eq(0)
.should('contain.text', 'To use expressions in n8n, follow these steps:');
aiAssistant.getters
.chatMessagesAssistant()
.eq(0)
.should(
'include.html',
`<pre><code class="language-json">[
{
"headers": {
"host": "n8n.instance.address",
...
},
"params": {},
"query": {},
"body": {
"name": "Jim",
"age": 30,
"city": "New York"
}
}
]
</code></pre>`,
);
aiAssistant.getters.codeSnippet().should('have.text', '{{$json.body.city}}');
});
it('should send current context to support chat', () => {
cy.createFixtureWorkflow('aiAssistant/workflows/simple_http_request_workflow.json');
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.actions.sendMessage('What is wrong with this workflow?');
cy.wait('@chatRequest').then((interception) => {
const { body } = interception.request;
// Body should contain the current workflow context
expect(body.payload).to.have.property('context');
expect(body.payload.context).to.have.property('currentView');
expect(body.payload.context.currentView.name).to.equal('NodeViewExisting');
expect(body.payload.context).to.have.property('currentWorkflow');
});
});
it('should not send workflow context if nothing changed', () => {
cy.createFixtureWorkflow('aiAssistant/workflows/simple_http_request_workflow.json');
cy.intercept('POST', '/rest/ai/chat', {
statusCode: 200,
fixture: 'aiAssistant/responses/simple_message_response.json',
}).as('chatRequest');
aiAssistant.getters.askAssistantFloatingButton().click();
aiAssistant.actions.sendMessage('What is wrong with this workflow?');
cy.wait('@chatRequest');
// Send another message without changing workflow or executing any node
aiAssistant.actions.sendMessage('And now?');
cy.wait('@chatRequest').then((interception) => {
const { body } = interception.request;
// Workflow context should be empty
expect(body.payload).to.have.property('context');
expect(body.payload.context).not.to.have.property('currentWorkflow');
});
// Update http request node url
wf.actions.openNode('HTTP Request');
ndv.actions.typeIntoParameterInput('url', 'https://example.com');
ndv.actions.close();
// Also execute the workflow
wf.actions.executeWorkflow();
// Send another message
aiAssistant.actions.sendMessage('What about now?');
cy.wait('@chatRequest').then((interception) => {
const { body } = interception.request;
// Both workflow and execution context should be sent
expect(body.payload).to.have.property('context');
expect(body.payload.context).to.have.property('currentWorkflow');
expect(body.payload.context.currentWorkflow).not.to.be.empty;
expect(body.payload.context).to.have.property('executionData');
expect(body.payload.context.executionData).not.to.be.empty;
});
});
});

View file

@ -0,0 +1,82 @@
import { EXECUTE_WORKFLOW_NODE_NAME } from '../constants';
import { WorkflowPage as WorkflowPageClass, NDV } from '../pages';
import { getVisiblePopper } from '../utils';
const workflowPage = new WorkflowPageClass();
const ndv = new NDV();
describe('Workflow Selector Parameter', () => {
beforeEach(() => {
cy.resetDatabase();
cy.signinAsOwner();
['Get_Weather', 'Search_DB'].forEach((workflowName) => {
workflowPage.actions.visit();
cy.createFixtureWorkflow(`Test_Subworkflow_${workflowName}.json`, workflowName);
workflowPage.actions.saveWorkflowOnButtonClick();
});
workflowPage.actions.visit();
workflowPage.actions.addInitialNodeToCanvas(EXECUTE_WORKFLOW_NODE_NAME, {
keepNdvOpen: true,
action: 'Call Another Workflow',
});
});
it('should render sub-workflows list', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
getVisiblePopper()
.should('have.length', 1)
.findChildByTestId('rlc-item')
.should('have.length', 2);
});
it('should show required parameter warning', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
ndv.getters.parameterInputIssues('workflowId').should('exist');
});
it('should filter sub-workflows list', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
ndv.getters.resourceLocatorSearch('workflowId').type('Weather');
getVisiblePopper()
.should('have.length', 1)
.findChildByTestId('rlc-item')
.should('have.length', 1)
.click();
ndv.getters
.resourceLocatorInput('workflowId')
.find('input')
.should('have.value', 'Get_Weather');
});
it('should render sub-workflow links correctly', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
getVisiblePopper().findChildByTestId('rlc-item').first().click();
ndv.getters.resourceLocatorInput('workflowId').find('a').should('exist');
cy.getByTestId('radio-button-expression').eq(1).click();
ndv.getters.resourceLocatorInput('workflowId').find('a').should('not.exist');
});
it('should switch to ID mode on expression', () => {
ndv.getters.resourceLocator('workflowId').should('be.visible');
ndv.getters.resourceLocatorInput('workflowId').click();
getVisiblePopper().findChildByTestId('rlc-item').first().click();
ndv.getters
.resourceLocatorModeSelector('workflowId')
.find('input')
.should('have.value', 'From list');
cy.getByTestId('radio-button-expression').eq(1).click();
ndv.getters
.resourceLocatorModeSelector('workflowId')
.find('input')
.should('have.value', 'By ID');
});
});

View file

@ -0,0 +1,35 @@
import { WorkflowsPage } from '../pages';
const workflowsPage = new WorkflowsPage();
describe('n8n.io iframe', () => {
describe('when telemetry is disabled', () => {
it('should not load the iframe when visiting /home/workflows', () => {
cy.overrideSettings({ telemetry: { enabled: false } });
cy.visit(workflowsPage.url);
cy.get('iframe').should('not.exist');
});
});
describe('when telemetry is enabled', () => {
it('should load the iframe when visiting /home/workflows', () => {
const testInstanceId = 'test-instance-id';
cy.overrideSettings({ telemetry: { enabled: true }, instanceId: testInstanceId });
const testUserId = Cypress.env('currentUserId');
const iframeUrl = `https://n8n.io/self-install?instanceId=${testInstanceId}&userId=${testUserId}`;
cy.intercept(iframeUrl, (req) => req.reply(200)).as('iframeRequest');
cy.visit(workflowsPage.url);
cy.get('iframe').should('exist').and('have.attr', 'src', iframeUrl);
cy.wait('@iframeRequest').its('response.statusCode').should('eq', 200);
});
});
});

View file

@ -1,10 +1,8 @@
import { getVisibleSelect } from '../utils'; import { setCredentialValues } from '../composables/modals/credential-modal';
import { clickCreateNewCredential } from '../composables/ndv';
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, NOTION_NODE_NAME } from '../constants'; import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, NOTION_NODE_NAME } from '../constants';
import { NDV, WorkflowPage } from '../pages'; import { NDV, WorkflowPage } from '../pages';
import { NodeCreator } from '../pages/features/node-creator'; import { NodeCreator } from '../pages/features/node-creator';
import { clickCreateNewCredential } from '../composables/ndv';
import { setCredentialValues } from '../composables/modals/credential-modal';
import { successToast } from '../pages/notifications';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -134,6 +132,11 @@ describe('NDV', () => {
'contains.text', 'contains.text',
"An expression here won't work because it uses .item and n8n can't figure out the matching item.", "An expression here won't work because it uses .item and n8n can't figure out the matching item.",
); );
ndv.getters.nodeRunErrorIndicator().should('be.visible');
ndv.getters.nodeRunTooltipIndicator().should('be.visible');
// The error details should be hidden behind a tooltip
ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Start Time');
ndv.getters.nodeRunTooltipIndicator().should('not.contain', 'Execution Time');
}); });
it('should save workflow using keyboard shortcut from NDV', () => { it('should save workflow using keyboard shortcut from NDV', () => {
@ -201,7 +204,7 @@ describe('NDV', () => {
.contains(key) .contains(key)
.should('be.visible'); .should('be.visible');
}); });
getObjectValueItem().find('label').click(); getObjectValueItem().find('label').click({ force: true });
expandedObjectProps.forEach((key) => { expandedObjectProps.forEach((key) => {
ndv.getters ndv.getters
.outputPanel() .outputPanel()
@ -339,38 +342,6 @@ describe('NDV', () => {
}); });
}); });
it('should not retrieve remote options when required params throw errors', () => {
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
ndv.getters.parameterInput('remoteOptions').click();
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
ndv.actions.setInvalidExpression({ fieldName: 'fieldId' });
ndv.getters.inputPanel().click(); // remove focus from input, hide expression preview
ndv.getters.parameterInput('remoteOptions').click();
ndv.getters.parameterInputIssues('remoteOptions').realHover({ scrollBehavior: false });
// Remote options dropdown should not be visible
ndv.getters.parameterInput('remoteOptions').find('.el-select').should('not.exist');
});
it('should retrieve remote options when non-required params throw errors', () => {
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
ndv.getters.parameterInput('remoteOptions').click();
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
ndv.getters.parameterInput('remoteOptions').click();
ndv.actions.setInvalidExpression({ fieldName: 'otherField' });
ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview
ndv.getters.parameterInput('remoteOptions').click();
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
});
it('should flag issues as soon as params are set', () => { it('should flag issues as soon as params are set', () => {
workflowPage.actions.addInitialNodeToCanvas('Webhook'); workflowPage.actions.addInitialNodeToCanvas('Webhook');
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();
@ -616,7 +587,13 @@ describe('NDV', () => {
ndv.getters.outputTableRow(1).find('mark').should('have.text', '<lib'); ndv.getters.outputTableRow(1).find('mark').should('have.text', '<lib');
ndv.getters.outputDisplayMode().find('label').eq(1).should('include.text', 'JSON'); ndv.getters.outputDisplayMode().find('label').eq(1).should('include.text', 'JSON');
ndv.getters.outputDisplayMode().find('label').eq(1).click(); ndv.getters
.outputDisplayMode()
.find('label')
.eq(1)
.scrollIntoView()
.should('be.visible')
.click();
ndv.getters.outputDataContainer().find('.json-data').should('exist'); ndv.getters.outputDataContainer().find('.json-data').should('exist');
ndv.getters ndv.getters
@ -631,8 +608,7 @@ describe('NDV', () => {
ndv.getters.outputDisplayMode().find('label').eq(2).click({ force: true }); ndv.getters.outputDisplayMode().find('label').eq(2).click({ force: true });
ndv.getters ndv.getters
.outputDataContainer() .outputDataContainer()
.findChildByTestId('run-data-schema-item') .findChildByTestId('run-data-schema-item-value')
.find('> span')
.should('include.text', '<?xml version="1.0" encoding="UTF-8"?>'); .should('include.text', '<?xml version="1.0" encoding="UTF-8"?>');
}); });
@ -642,8 +618,10 @@ describe('NDV', () => {
// Should not show run info before execution // Should not show run info before execution
ndv.getters.nodeRunSuccessIndicator().should('not.exist'); ndv.getters.nodeRunSuccessIndicator().should('not.exist');
ndv.getters.nodeRunErrorIndicator().should('not.exist'); ndv.getters.nodeRunErrorIndicator().should('not.exist');
ndv.getters.nodeRunTooltipIndicator().should('not.exist');
ndv.getters.nodeExecuteButton().click(); ndv.getters.nodeExecuteButton().click();
ndv.getters.nodeRunSuccessIndicator().should('exist'); ndv.getters.nodeRunSuccessIndicator().should('exist');
ndv.getters.nodeRunTooltipIndicator().should('exist');
}); });
it('should properly show node execution indicator for multiple nodes', () => { it('should properly show node execution indicator for multiple nodes', () => {
@ -655,6 +633,7 @@ describe('NDV', () => {
// Manual tigger node should show success indicator // Manual tigger node should show success indicator
workflowPage.actions.openNode('When clicking Test workflow'); workflowPage.actions.openNode('When clicking Test workflow');
ndv.getters.nodeRunSuccessIndicator().should('exist'); ndv.getters.nodeRunSuccessIndicator().should('exist');
ndv.getters.nodeRunTooltipIndicator().should('exist');
// Code node should show error // Code node should show error
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.actions.openNode('Code'); workflowPage.actions.openNode('Code');
@ -699,6 +678,23 @@ describe('NDV', () => {
ndv.getters.parameterInput('operation').find('input').should('have.value', 'Delete'); ndv.getters.parameterInput('operation').find('input').should('have.value', 'Delete');
}); });
it('Should show a notice when remote options cannot be fetched because of missing credentials', () => {
cy.intercept('POST', '/rest/dynamic-node-parameters/options', { statusCode: 403 }).as(
'parameterOptions',
);
workflowPage.actions.addInitialNodeToCanvas(NOTION_NODE_NAME, {
keepNdvOpen: true,
action: 'Update a database page',
});
ndv.actions.addItemToFixedCollection('propertiesUi');
ndv.getters
.parameterInput('key')
.find('input')
.should('have.value', 'Set up credential to see options');
});
it('Should show error state when remote options cannot be fetched', () => { it('Should show error state when remote options cannot be fetched', () => {
cy.intercept('POST', '/rest/dynamic-node-parameters/options', { statusCode: 500 }).as( cy.intercept('POST', '/rest/dynamic-node-parameters/options', { statusCode: 500 }).as(
'parameterOptions', 'parameterOptions',
@ -709,6 +705,11 @@ describe('NDV', () => {
action: 'Update a database page', action: 'Update a database page',
}); });
clickCreateNewCredential();
setCredentialValues({
apiKey: 'sk_test_123',
});
ndv.actions.addItemToFixedCollection('propertiesUi'); ndv.actions.addItemToFixedCollection('propertiesUi');
ndv.getters ndv.getters
.parameterInput('key') .parameterInput('key')
@ -739,23 +740,6 @@ describe('NDV', () => {
}); });
}); });
it('Stop listening for trigger event from NDV', () => {
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
workflowPage.actions.addInitialNodeToCanvas('Local File Trigger', {
keepNdvOpen: true,
action: 'On Changes To A Specific File',
isTrigger: true,
});
ndv.getters.triggerPanelExecuteButton().should('exist');
ndv.getters.triggerPanelExecuteButton().realClick();
ndv.getters.triggerPanelExecuteButton().should('contain', 'Stop Listening');
ndv.getters.triggerPanelExecuteButton().realClick();
cy.wait('@workflowRun').then(() => {
ndv.getters.triggerPanelExecuteButton().should('contain', 'Test step');
successToast().should('exist');
});
});
it('should allow selecting item for expressions', () => { it('should allow selecting item for expressions', () => {
workflowPage.actions.visit(); workflowPage.actions.visit();
@ -782,4 +766,33 @@ describe('NDV', () => {
ndv.actions.expressionSelectItem(1); ndv.actions.expressionSelectItem(1);
ndv.getters.inlineExpressionEditorOutput().should('have.text', '1'); ndv.getters.inlineExpressionEditorOutput().should('have.text', '1');
}); });
it('should show data from the correct output in schema view', () => {
cy.createFixtureWorkflow('Test_workflow_multiple_outputs.json');
workflowPage.actions.zoomToFit();
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Only Item 1');
ndv.getters.inputPanel().should('be.visible');
ndv.getters
.inputPanel()
.find('[data-test-id=run-data-schema-item]')
.should('contain.text', 'onlyOnItem1');
ndv.actions.close();
workflowPage.actions.openNode('Only Item 2');
ndv.getters.inputPanel().should('be.visible');
ndv.getters
.inputPanel()
.find('[data-test-id=run-data-schema-item]')
.should('contain.text', 'onlyOnItem2');
ndv.actions.close();
workflowPage.actions.openNode('Only Item 3');
ndv.getters.inputPanel().should('be.visible');
ndv.getters
.inputPanel()
.find('[data-test-id=run-data-schema-item]')
.should('contain.text', 'onlyOnItem3');
});
}); });

View file

@ -1,7 +1,8 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV } from '../pages/ndv'; import { NDV } from '../pages/ndv';
import { successToast } from '../pages/notifications'; import { successToast } from '../pages/notifications';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
const ndv = new NDV(); const ndv = new NDV();
@ -38,31 +39,64 @@ describe('Code node', () => {
successToast().contains('Node executed successfully'); successToast().contains('Node executed successfully');
}); });
it('should show lint errors in `runOnceForAllItems` mode', () => {
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
const getEditor = () => getParameter().find('.cm-content').should('exist');
getEditor()
.type('{selectall}')
.paste(`$input.itemMatching()
$input.item
$('When clicking Test workflow').item
$input.first(1)
for (const item of $input.all()) {
item.foo
}
return
`);
getParameter().get('.cm-lint-marker-error').should('have.length', 6);
getParameter().contains('itemMatching').realHover();
cy.get('.cm-tooltip-lint').should(
'have.text',
'`.itemMatching()` expects an item index to be passed in as its argument.',
);
});
it('should show lint errors in `runOnceForEachItem` mode', () => {
const getParameter = () => ndv.getters.parameterInput('jsCode').should('be.visible');
const getEditor = () => getParameter().find('.cm-content').should('exist');
ndv.getters.parameterInput('mode').click();
ndv.actions.selectOptionInParameterDropdown('mode', 'Run Once for Each Item');
getEditor()
.type('{selectall}')
.paste(`$input.itemMatching()
$input.all()
$input.first()
$input.item()
return []
`);
getParameter().get('.cm-lint-marker-error').should('have.length', 5);
getParameter().contains('all').realHover();
cy.get('.cm-tooltip-lint').should(
'have.text',
"Method `$input.all()` is only available in the 'Run Once for All Items' mode.",
);
});
}); });
describe('Ask AI', () => { describe('Ask AI', () => {
it('tab should display based on experiment', () => {
WorkflowPage.actions.visit();
cy.window().then((win) => {
win.featureFlags.override('011_ask_AI', 'control');
WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Code');
WorkflowPage.actions.openNode('Code');
cy.getByTestId('code-node-tab-ai').should('not.exist');
ndv.actions.close();
win.featureFlags.override('011_ask_AI', undefined);
WorkflowPage.actions.openNode('Code');
cy.getByTestId('code-node-tab-ai').should('not.exist');
});
});
describe('Enabled', () => { describe('Enabled', () => {
beforeEach(() => { beforeEach(() => {
cy.enableFeature('askAi');
WorkflowPage.actions.visit(); WorkflowPage.actions.visit();
cy.window().then((win) => {
win.featureFlags.override('011_ask_AI', 'gpt3'); cy.window().then(() => {
WorkflowPage.actions.addInitialNodeToCanvas('Manual'); WorkflowPage.actions.addInitialNodeToCanvas('Manual');
WorkflowPage.actions.addNodeToCanvas('Code', true, true); WorkflowPage.actions.addNodeToCanvas('Code', true, true);
}); });
@ -107,7 +141,7 @@ describe('Code node', () => {
cy.getByTestId('ask-ai-prompt-input').type(prompt); cy.getByTestId('ask-ai-prompt-input').type(prompt);
cy.intercept('POST', '/rest/ask-ai', { cy.intercept('POST', '/rest/ai/ask-ai', {
statusCode: 200, statusCode: 200,
body: { body: {
data: { data: {
@ -119,9 +153,7 @@ describe('Code node', () => {
cy.getByTestId('ask-ai-cta').click(); cy.getByTestId('ask-ai-cta').click();
const askAiReq = cy.wait('@ask-ai'); const askAiReq = cy.wait('@ask-ai');
askAiReq askAiReq.its('request.body').should('have.keys', ['question', 'context', 'forNode']);
.its('request.body')
.should('have.keys', ['question', 'model', 'context', 'n8nVersion']);
askAiReq.its('context').should('have.keys', ['schema', 'ndvPushRef', 'pushRef']); askAiReq.its('context').should('have.keys', ['schema', 'ndvPushRef', 'pushRef']);
@ -130,22 +162,22 @@ describe('Code node', () => {
cy.get('#tab-code').should('have.class', 'is-active'); cy.get('#tab-code').should('have.class', 'is-active');
}); });
it('should show error based on status code', () => { const handledCodes = [
const prompt = nanoid(20); { code: 400, message: 'Code generation failed due to an unknown reason' },
cy.get('#tab-ask-ai').click(); { code: 413, message: 'Your workflow data is too large for AI to process' },
ndv.actions.executePrevious(); { code: 429, message: "We've hit our rate limit with our AI partner" },
{ code: 500, message: 'Code generation failed due to an unknown reason' },
];
cy.getByTestId('ask-ai-prompt-input').type(prompt); handledCodes.forEach(({ code, message }) => {
it(`should show error based on status code ${code}`, () => {
const prompt = nanoid(20);
cy.get('#tab-ask-ai').click();
ndv.actions.executePrevious();
const handledCodes = [ cy.getByTestId('ask-ai-prompt-input').type(prompt);
{ code: 400, message: 'Code generation failed due to an unknown reason' },
{ code: 413, message: 'Your workflow data is too large for AI to process' },
{ code: 429, message: "We've hit our rate limit with our AI partner" },
{ code: 500, message: 'Code generation failed due to an unknown reason' },
];
handledCodes.forEach(({ code, message }) => { cy.intercept('POST', '/rest/ai/ask-ai', {
cy.intercept('POST', '/rest/ask-ai', {
statusCode: code, statusCode: code,
status: code, status: code,
}).as('ask-ai'); }).as('ask-ai');

View file

@ -5,11 +5,11 @@ import {
EDIT_FIELDS_SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME,
NOTION_NODE_NAME, NOTION_NODE_NAME,
} from '../constants'; } from '../constants';
import { WorkflowExecutionsTab } from '../pages';
import { errorToast, successToast } from '../pages/notifications';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows'; import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
import { WorkflowExecutionsTab } from '../pages';
import { errorToast, successToast } from '../pages/notifications';
const NEW_WORKFLOW_NAME = 'Something else'; const NEW_WORKFLOW_NAME = 'Something else';
const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow'; const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow';

View file

@ -0,0 +1,133 @@
{
"name": "PAY-1707",
"nodes": [
{
"parameters": {
"options": {}
},
"id": "eaa428a8-eb9d-478a-b997-aed6ed298507",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
920,
380
]
},
{
"parameters": {
"options": {}
},
"id": "6b285c91-e7ea-4943-8ba3-59ce01a35d20",
"name": "Edit Fields1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
920,
540
]
},
{
"parameters": {
"jsCode": "return Array.from({length: 5}, _ => ({}))"
},
"id": "70e682aa-dfef-4db7-a158-971ec7976d49",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
380
]
},
{
"parameters": {
"jsCode": "return Array.from({length: 5}, _ => ({}))"
},
"id": "d5ee979e-9f53-4e62-8eb2-cdb92be8ea6e",
"name": "Code1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
540
]
},
{
"parameters": {
"path": "dd660366-ca4a-4736-8b1f-454560e87bfb",
"options": {}
},
"id": "20c33c8a-ab2f-4dd4-990f-6390feeb840c",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
480,
440
],
"webhookId": "dd660366-ca4a-4736-8b1f-454560e87bfb"
}
],
"pinData": {
"Code1": [
{
"json": {}
},
{
"json": {}
}
]
},
"connections": {
"Code1": {
"main": [
[
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
},
{
"node": "Code1",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "01e6693e-54f3-432d-9b1f-922ef92b4ab6",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "8a47b83b4479b11330fdf21ccc96d4a8117035a968612e452b4c87bfd09c16c7"
},
"id": "hU0gp19G29ehWktc",
"tags": []
}

View file

@ -0,0 +1,39 @@
{
"nodes": [
{
"parameters": {
"path": "FwrbSiaua2Xmvn6-Z-7CQ",
"options": {}
},
"id": "8fcc7e5f-2cef-4938-9564-eea504c20aa0",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
360,
220
],
"webhookId": "9c778f2a-e882-46ed-a0e4-c8e2f76ccd65"
}
],
"connections": {},
"pinData": {
"Webhook": [
{
"headers": {
"connection": "keep-alive",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"accept": "*/*",
"cookie": "n8n-auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjNiM2FhOTE5LWRhZDgtNDE5MS1hZWZiLTlhZDIwZTZkMjJjNiIsImhhc2giOiJ1ZVAxR1F3U2paIiwiaWF0IjoxNzI4OTE1NTQyLCJleHAiOjE3Mjk1MjAzNDJ9.fV02gpUnSiUoMxHwfB0npBjcjct7Mv9vGfj-jRTT3-I",
"host": "localhost:5678",
"accept-encoding": "gzip, deflate"
},
"params": {},
"query": {},
"body": {},
"webhookUrl": "http://localhost:5678/webhook-test/FwrbSiaua2Xmvn6-Z-7CQ",
"executionMode": "test"
}
]
}
}

View file

@ -0,0 +1,150 @@
{
"meta": {
"instanceId": "777c68374367604fdf2a0bcfe9b1b574575ddea61aa8268e4bf034434bd7c894"
},
"nodes": [
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "0effebfc-fa8c-4d41-8a37-6d5695dfc9ee",
"name": "test",
"value": "test",
"type": "string"
},
{
"id": "beb8723f-6333-4186-ab88-41d4e2338866",
"name": "test",
"value": "test",
"type": "string"
},
{
"id": "85095836-4e94-442f-9270-e1a89008c129",
"name": "test",
"value": "test",
"type": "string"
},
{
"id": "b6163f8a-bca6-4364-8b38-182df37c55cd",
"name": "=should be visible!",
"value": "=not visible",
"type": "string"
}
]
},
"options": {}
},
"id": "950fcdc1-9e92-410f-8377-d4240e9bf6ff",
"name": "Edit Fields1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
680,
460
]
},
{
"parameters": {
"messageType": "block",
"blocksUi": "blocks",
"text": "=should be visible",
"otherOptions": {
"sendAsUser": "=not visible"
}
},
"id": "dcf7410d-0f8e-4cdb-9819-ae275558bdaa",
"name": "Slack",
"type": "n8n-nodes-base.slack",
"typeVersion": 2.2,
"position": [
900,
460
],
"webhookId": "002b502e-31e5-4fdb-ac43-a56cfde8f82a"
},
{
"parameters": {
"rule": {
"interval": [
{},
{
"field": "=should be visible"
},
{
"field": "=not visible"
}
]
}
},
"id": "4c948a3f-19d4-4b08-a8be-f7d2964a21f4",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
460,
460
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "5dcaab37-1146-49c6-97a3-3b2f73483270",
"name": "object",
"value": "=1 visible!\n2 {\n3 \"str\": \"two\",\n4 \"str_date\": \"{{ $now }}\",\n5 \"str_int\": \"1\",\n6 \"str_float\": \"1.234\",\n7 not visible!\n \"str_bool\": \"true\",\n \"str_email\": \"david@thedavid.com\",\n \"str_with_email\":\"My email is david@n8n.io\",\n \"str_json_single\":\"{'one':'two'}\",\n \"str_json_double\":\"{\\\"one\\\":\\\"two\\\"}\",\n \"bool\": true,\n \"list\": [1, 2, 3],\n \"decimal\": 1.234,\n \"timestamp1\": 1708695471,\n \"timestamp2\": 1708695471000,\n \"timestamp3\": 1708695471000000,\n \"num_one\": 1\n}",
"type": "object"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "a41dfb0d-38aa-42d2-b3e2-1854090bd319",
"name": "With long expression",
"type": "n8n-nodes-base.set",
"typeVersion": 3.3,
"position": [
1100,
460
]
}
],
"connections": {
"Edit Fields1": {
"main": [
[
{
"node": "Slack",
"type": "main",
"index": 0
}
]
]
},
"Slack": {
"main": [
[
{
"node": "With long expression",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}

View file

@ -0,0 +1,53 @@
{
"name": "Get Weather",
"nodes": [
{
"parameters": {},
"id": "82eed1ba-179b-4f8f-8a85-b45f0d4e5857",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1,
"position": [
560,
340
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "6ad8dc55-20f3-45af-a724-c7ecac90d338",
"name": "response",
"value": "Weather is sunny",
"type": "string"
}
]
},
"options": {}
},
"id": "8f3e00f6-fc92-4aba-817b-93d206158bda",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
780,
340
]
}
],
"pinData": {},
"connections": {
"Execute Workflow Trigger": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
}
}

View file

@ -0,0 +1,64 @@
{
"name": "Search DB",
"nodes": [
{
"parameters": {},
"id": "64465f9b-63de-43f9-8d90-b5b2eb7a2dc7",
"name": "Execute Workflow Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1,
"position": [
640,
380
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "6ad8dc55-20f3-45af-a724-c7ecac90d338",
"name": "response",
"value": "10 results found",
"type": "string"
}
]
},
"options": {}
},
"id": "b580fd2b-00c8-4a52-8acb-024f204c0947",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
860,
380
]
}
],
"pinData": {},
"connections": {
"Execute Workflow Trigger": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "6026f7a4-f5dc-4c27-9f83-3a02fc6e33ae",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
},
"id": "BFFhCdBZmNSkx4qf",
"tags": []
}

View file

@ -0,0 +1,223 @@
{
"name": "Multiple outputs",
"nodes": [
{
"parameters": {},
"id": "64b27674-3da6-46ce-9008-e173182efa48",
"name": "When clicking Test workflow",
"type": "n8n-nodes-base.manualTrigger",
"position": [
16,
-32
],
"typeVersion": 1
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"leftValue": "={{ $json.code }}",
"rightValue": 1,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Item1"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "a659050f-0867-471d-8914-d499b6ad7b31",
"leftValue": "={{ $json.code }}",
"rightValue": 2,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Item2"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "109fc001-53af-48f1-b79c-5e9afc8b94bd",
"leftValue": "={{ $json.code }}",
"rightValue": 3,
"operator": {
"type": "number",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Item3"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.switch",
"position": [
192,
-32
],
"id": "3863cc7a-8f45-46fc-a60c-36aad5b12877",
"name": "Switch",
"typeVersion": 3
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "f71bac89-8852-41b2-98dd-cb689f011dcb",
"name": "",
"value": "",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"position": [
480,
-192
],
"id": "85940094-4656-4cdf-a871-1b3b46421de3",
"name": "Only Item 1",
"typeVersion": 3.4
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.set",
"position": [
480,
-32
],
"id": "a7f4e2b5-8cc9-4881-aa06-38601988740e",
"name": "Only Item 2",
"typeVersion": 3.4
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.set",
"position": [
480,
128
],
"id": "7e44ad56-415a-4991-a70e-fea86c430031",
"name": "Only Item 3",
"typeVersion": 3.4
}
],
"pinData": {
"When clicking Test workflow": [
{
"json": {
"name": "First item",
"onlyOnItem1": true,
"code": 1
}
},
{
"json": {
"name": "Second item",
"onlyOnItem2": true,
"code": 2
}
},
{
"json": {
"name": "Third item",
"onlyOnItem3": true,
"code": 3
}
}
]
},
"connections": {
"When clicking Test workflow": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
},
"Switch": {
"main": [
[
{
"node": "Only Item 1",
"type": "main",
"index": 0
}
],
[
{
"node": "Only Item 2",
"type": "main",
"index": 0
}
],
[
{
"node": "Only Item 3",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "1e2a7b45-7730-42d6-989e-f3fa80de303e",
"meta": {
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
},
"id": "V2ld4YU11fsHgr1z",
"tags": []
}

View file

@ -0,0 +1,8 @@
{
"data": {
"sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-emTezIGat7bQsDdtIlbti",
"parameters": {
"jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.myNewField = 1;\n}\n\nreturn $input.all();"
}
}
}

View file

@ -0,0 +1,23 @@
{
"sessionId": "1",
"messages": [
{
"role": "assistant",
"type": "message",
"text": "Hi there! Here is my top solution to fix the error in your **Code** node 👇"
},
{
"type": "code-diff",
"description": "Fix the syntax error by changing '1asd' to a valid value. In this case, it seems like '1' was intended.",
"suggestionId": "1",
"codeDiff": "@@ -2,2 +2,2 @@\n item.json.myNewField = 1asd;\n+ item.json.myNewField = 1;\n",
"role": "assistant",
"quickReplies": [
{
"text": "Give me another solution",
"type": "new-suggestion"
}
]
}
]
}

View file

@ -0,0 +1,28 @@
{
"sessionId": "f1d19ed5-0d55-4bad-b49a-f0c56bd6f76f-705b5dbf-12d4-4805-87a3-1e5b3c716d29-W1JgVNrpfitpSNF9rAjB4",
"messages": [
{
"role": "assistant",
"type": "message",
"text": "To use expressions in n8n, follow these steps:\n\n1. Hover over the parameter where you want to use an expression.\n2. Select **Expressions** in the **Fixed/Expression** toggle.\n3. Write your expression in the parameter, or select **Open expression editor** to open the expressions editor. You can browse the available data in the **Variable selector**. All expressions have the format `{{ your expression here }}`.\n\n### Example: Get data from webhook body\n\nIf your webhook data looks like this:\n\n```json\n[\n {\n \"headers\": {\n \"host\": \"n8n.instance.address\",\n ...\n },\n \"params\": {},\n \"query\": {},\n \"body\": {\n \"name\": \"Jim\",\n \"age\": 30,\n \"city\": \"New York\"\n }\n }\n]\n```\n\nYou can use the following expression to get the value of `city`:\n\n```js\n{{$json.body.city}}\n```\n\nThis expression accesses the incoming JSON-formatted data using n8n's custom `$json` variable and finds the value of `city` (in this example, \"New York\").",
"codeSnippet": "{{$json.body.city}}"
},
{
"role": "assistant",
"type": "message",
"text": "Did this answer solve your question?",
"quickReplies": [
{
"text": "Yes, thanks",
"type": "all-good",
"isFeedback": true
},
{
"text": "No, I am still stuck",
"type": "still-stuck",
"isFeedback": true
}
]
}
]
}

View file

@ -0,0 +1,16 @@
{
"sessionId": "1",
"messages": [
{
"role": "assistant",
"type": "message",
"title": "Glad to Help",
"text": "I'm glad I could help. If you have any more questions or need further assistance with your n8n workflows, feel free to ask!"
},
{
"role": "assistant",
"type": "event",
"eventName": "end-session"
}
]
}

View file

@ -0,0 +1,20 @@
{
"sessionId": "1",
"messages": [
{
"role": "assistant",
"type": "message",
"text": "It seems like my suggestion did not work. Do you want me to come up with a different suggestion? You can also provide more context via the chat.",
"quickReplies": [
{
"text": "Yes",
"type": "new-suggestion"
},
{
"text": "No, I don't think you can help",
"type": "event:end-session"
}
]
}
]
}

View file

@ -0,0 +1,20 @@
{
"sessionId": "1",
"messages": [
{
"role": "assistant",
"type": "message",
"text": "Hey, this is an assistant message",
"quickReplies": [
{
"text": "Sure, let's do it",
"type": "yes"
},
{
"text": "Nah, doesn't sound good",
"type": "no"
}
]
}
]
}

View file

@ -0,0 +1,10 @@
{
"sessionId": "1",
"messages": [
{
"role": "assistant",
"type": "message",
"text": "Hey, this is an assistant message"
}
]
}

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