mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge branch 'master' into node-1608-credential-parameters-tech-debt-project
This commit is contained in:
commit
2d8b62f316
|
@ -9,7 +9,7 @@
|
|||
"type=bind,source=${localEnv:HOME}/.n8n,target=/home/node/.n8n,consistency=cached"
|
||||
],
|
||||
"forwardPorts": [8080, 5678],
|
||||
"postCreateCommand": "corepack prepare --activate && pnpm install ",
|
||||
"postCreateCommand": "corepack prepare --activate && pnpm install",
|
||||
"postAttachCommand": "pnpm build",
|
||||
"customizations": {
|
||||
"codespaces": {
|
||||
|
|
45
.github/pull_request_title_conventions.md
vendored
45
.github/pull_request_title_conventions.md
vendored
|
@ -4,16 +4,16 @@ We have very precise rules over how Pull Requests (to the `master` branch) must
|
|||
|
||||
A PR title consists of these elements:
|
||||
|
||||
```
|
||||
```text
|
||||
<type>(<scope>): <summary>
|
||||
│ │ │
|
||||
│ │ └─⫸ Summary: In imperative present tense.
|
||||
| | Capitalized
|
||||
| | No period at the end.
|
||||
│ │
|
||||
│ └─⫸ Scope: API|core|editor|* Node|benchmark
|
||||
│ └─⫸ 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
|
||||
|
@ -27,35 +27,38 @@ A PR title consists of these elements:
|
|||
|
||||
The structure looks like this:
|
||||
|
||||
### **Type**
|
||||
## Type
|
||||
|
||||
Must be one of the following:
|
||||
|
||||
- `feat` - A new feature
|
||||
- `fix` - A bug fix
|
||||
- `perf` - A code change that improves performance
|
||||
- `test` - Adding missing tests or correcting existing tests
|
||||
- `docs` - Documentation only changes
|
||||
- `refactor` - A code change that neither fixes a bug nor adds a feature
|
||||
- `build` - Changes that affect the build system or external dependencies (example scopes: broccoli, npm)
|
||||
- `ci` - Changes to our CI configuration files and scripts (e.g. Github actions)
|
||||
| type | description | appears in changelog |
|
||||
| --- | --- | --- |
|
||||
| `feat` | A new feature | ✅ |
|
||||
| `fix` | A bug fix | ✅ |
|
||||
| `perf` | A code change that improves performance | ✅ |
|
||||
| `test` | Adding missing tests or correcting existing tests | ❌ |
|
||||
| `docs` | Documentation only changes | ❌ |
|
||||
| `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!)
|
||||
|
||||
- `API` - changes to the _public_ API
|
||||
- `benchmark` - changes to the benchmark cli
|
||||
- `core` - changes to the core / private API / backend of n8n
|
||||
- `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.
|
||||
- mattermost → Mattermost Node
|
||||
- microsoftToDo → Microsoft To Do Node
|
||||
- n8n → n8n Node
|
||||
- `benchmark` - changes to the Benchmark cli
|
||||
|
||||
### **Summary**
|
||||
## Summary
|
||||
|
||||
The summary contains succinct description of the change:
|
||||
|
||||
|
@ -65,15 +68,15 @@ The summary contains succinct description of the change:
|
|||
- 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.
|
||||
|
||||
### **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.
|
||||
|
||||
### **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:
|
||||
|
||||
```
|
||||
```text
|
||||
BREAKING CHANGE: <breaking change summary>
|
||||
<BLANK LINE>
|
||||
<breaking change description + migration instructions>
|
||||
|
@ -84,7 +87,7 @@ Fixes #<issue number>
|
|||
|
||||
or
|
||||
|
||||
```
|
||||
```text
|
||||
DEPRECATED: <what is deprecated>
|
||||
<BLANK LINE>
|
||||
<deprecation description + recommended update path>
|
||||
|
@ -103,7 +106,7 @@ A Breaking Change section should start with the phrase "`BREAKING CHANGE:` " fol
|
|||
|
||||
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.
|
||||
|
||||
|
|
6
.github/scripts/update-changelog.mjs
vendored
6
.github/scripts/update-changelog.mjs
vendored
|
@ -16,7 +16,11 @@ const changelogStream = conventionalChangelog({
|
|||
releaseCount: 1,
|
||||
tagPrefix: 'n8n@',
|
||||
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) => {
|
||||
console.error(err.stack);
|
||||
|
|
|
@ -2,13 +2,17 @@ name: Destroy Benchmark Env
|
|||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 1 * * *'
|
||||
- cron: '0 5 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: benchmark
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
48
.github/workflows/benchmark-nightly.yml
vendored
48
.github/workflows/benchmark-nightly.yml
vendored
|
@ -1,9 +1,9 @@
|
|||
name: Run Nightly Benchmark
|
||||
run-name: Benchmark ${{ inputs.n8n_tag }}
|
||||
run-name: Benchmark ${{ inputs.n8n_tag || 'nightly' }}
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
- cron: '30 1,2,3 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug:
|
||||
|
@ -23,12 +23,18 @@ 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 }}
|
||||
K6_API_TOKEN: ${{ secrets.K6_API_TOKEN }}
|
||||
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
|
||||
|
@ -58,12 +64,38 @@ jobs:
|
|||
tenant-id: ${{ env.ARM_TENANT_ID }}
|
||||
subscription-id: ${{ env.ARM_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Run the benchmark with debug logging
|
||||
if: github.event.inputs.debug == 'true'
|
||||
run: pnpm run-in-cloud --debug
|
||||
- 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
|
||||
if: github.event.inputs.debug != 'true'
|
||||
run: pnpm run-in-cloud
|
||||
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
|
||||
|
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- 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
|
||||
|
||||
|
|
2
.github/workflows/check-pr-title.yml
vendored
2
.github/workflows/check-pr-title.yml
vendored
|
@ -28,6 +28,6 @@ jobs:
|
|||
|
||||
- name: 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:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
42
.github/workflows/chromatic.yml
vendored
42
.github/workflows/chromatic.yml
vendored
|
@ -4,19 +4,49 @@ on:
|
|||
workflow_dispatch:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- packages/design-system/**
|
||||
- .github/workflows/chromatic.yml
|
||||
|
||||
concurrency:
|
||||
group: chromatic-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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:
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
|
|
3
.github/workflows/ci-postgres-mysql.yml
vendored
3
.github/workflows/ci-postgres-mysql.yml
vendored
|
@ -10,8 +10,6 @@ on:
|
|||
- .github/workflows/ci-postgres-mysql.yml
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
branches:
|
||||
- 'release/*'
|
||||
|
||||
concurrency:
|
||||
group: db-${{ github.event.pull_request.number || github.ref }}
|
||||
|
@ -21,6 +19,7 @@ jobs:
|
|||
build:
|
||||
name: Install & Build
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request_review' || startsWith(github.event.pull_request.base.ref, 'release/')
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
- run: corepack enable
|
||||
|
|
3
.github/workflows/ci-pull-requests.yml
vendored
3
.github/workflows/ci-pull-requests.yml
vendored
|
@ -30,6 +30,9 @@ jobs:
|
|||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Run formatcheck
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Run typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
|
|
9
.github/workflows/docker-images-nightly.yml
vendored
9
.github/workflows/docker-images-nightly.yml
vendored
|
@ -32,6 +32,9 @@ on:
|
|||
required: false
|
||||
default: ''
|
||||
|
||||
env:
|
||||
N8N_TAG: ${{ inputs.tag || 'nightly' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -76,10 +79,10 @@ jobs:
|
|||
push: true
|
||||
cache-from: type=gha
|
||||
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: github.event.inputs.tag == 'nightly'
|
||||
if: env.N8N_TAG == 'nightly'
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
|
@ -87,7 +90,7 @@ jobs:
|
|||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Push image to GHCR
|
||||
if: github.event.inputs.tag == 'nightly'
|
||||
if: env.N8N_TAG == 'nightly'
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag ghcr.io/${{ github.repository_owner }}/n8n:nightly \
|
||||
|
|
44
.github/workflows/e2e-tests-pr.yml
vendored
44
.github/workflows/e2e-tests-pr.yml
vendored
|
@ -3,19 +3,51 @@ name: PR E2E
|
|||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release/*'
|
||||
|
||||
concurrency:
|
||||
group: e2e-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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:
|
||||
name: E2E [Electron/Node 18]
|
||||
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:
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
user: ${{ github.event.pull_request.user.login || 'PR User' }}
|
||||
|
@ -25,11 +57,11 @@ jobs:
|
|||
post-e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: E2E [Electron/Node 18] - Checks
|
||||
needs: [run-e2e-tests]
|
||||
needs: [get-metadata, run-e2e-tests]
|
||||
if: always()
|
||||
steps:
|
||||
- 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
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
|
|
@ -7,3 +7,11 @@ packages/nodes-base/nodes/**/test
|
|||
packages/cli/templates/form-trigger.handlebars
|
||||
cypress/fixtures
|
||||
CHANGELOG.md
|
||||
.github/pull_request_template.md
|
||||
# Ignored for now
|
||||
**/*.md
|
||||
# Handled by biome
|
||||
**/*.ts
|
||||
**/*.js
|
||||
**/*.json
|
||||
**/*.jsonc
|
||||
|
|
1
.vscode/extensions.json
vendored
1
.vscode/extensions.json
vendored
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"biomejs.biome",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"dangmai.workspace-default-settings",
|
||||
"dbaeumer.vscode-eslint",
|
||||
|
|
16
.vscode/settings.default.json
vendored
16
.vscode/settings.default.json
vendored
|
@ -1,6 +1,22 @@
|
|||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"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": {
|
||||
"node_modules": true,
|
||||
"dist": true,
|
||||
|
|
129
CHANGELOG.md
129
CHANGELOG.md
|
@ -1,3 +1,130 @@
|
|||
# [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)
|
||||
|
||||
|
||||
|
@ -27,6 +154,8 @@
|
|||
|
||||
* **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)
|
||||
|
|
51
biome.jsonc
Normal file
51
biome.jsonc
Normal 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
7
cypress/biome.jsonc
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "../node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"extends": ["../biome.jsonc"],
|
||||
"formatter": {
|
||||
"ignore": ["fixtures/**"]
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { ROUTES } from '../constants';
|
||||
import { getManualChatModal } from './modals/chat-modal';
|
||||
import { ROUTES } from '../constants';
|
||||
|
||||
/**
|
||||
* Types
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
||||
import { getUniqueWorkflowName } from '../utils/workflowUtils';
|
||||
|
||||
const WorkflowsPage = new WorkflowsPageClass();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { SettingsLogStreamingPage } from '../pages';
|
||||
import { getVisibleModalOverlay } from '../utils/modal';
|
||||
import { getVisibleDropdown } from '../utils';
|
||||
import { getVisibleModalOverlay } from '../utils/modal';
|
||||
|
||||
const settingsLogStreamingPage = new SettingsLogStreamingPage();
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@ import {
|
|||
SET_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
|
||||
import { NDV } from '../pages/ndv';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
// Suite-specific constants
|
||||
const CODE_NODE_NEW_NAME = 'Something else';
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { successToast } from '../pages/notifications';
|
||||
import {
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
|
@ -9,6 +7,8 @@ import {
|
|||
IF_NODE_NAME,
|
||||
HTTP_REQUEST_NODE_NAME,
|
||||
} from './../constants';
|
||||
import { successToast } from '../pages/notifications';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
describe('Canvas Actions', () => {
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { NDV, WorkflowExecutionsTab } from '../pages';
|
||||
import {
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
|
@ -9,6 +7,8 @@ import {
|
|||
SWITCH_NODE_NAME,
|
||||
MERGE_NODE_NAME,
|
||||
} from './../constants';
|
||||
import { NDV, WorkflowExecutionsTab } from '../pages';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
const ExecutionsTab = new WorkflowExecutionsTab();
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import {
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
SCHEDULE_TRIGGER_NODE_NAME,
|
||||
} from './../constants';
|
||||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||
import { WorkflowPage, NDV, CredentialsModal } from '../pages';
|
||||
import { cowBase64 } from '../support/binaryTestFiles';
|
||||
import { BACKEND_BASE_URL, EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import * as projects from '../composables/projects';
|
||||
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN, NOTION_NODE_NAME } from '../constants';
|
||||
import {
|
||||
CredentialsModal,
|
||||
|
@ -8,7 +9,6 @@ import {
|
|||
WorkflowsPage,
|
||||
} from '../pages';
|
||||
import { getVisibleDropdown, getVisiblePopper, getVisibleSelect } from '../utils';
|
||||
import * as projects from '../composables/projects';
|
||||
|
||||
/**
|
||||
* User U1 - Instance owner
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN } from '../constants';
|
||||
import { MainSidebar, SettingsSidebar, SettingsUsersPage } from '../pages';
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
import { PersonalSettingsPage } from '../pages/settings-personal';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
|
||||
/**
|
||||
* User A - Instance owner
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
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';
|
||||
|
||||
const workflowPage = new WorkflowPageClass();
|
||||
|
@ -503,7 +503,7 @@ describe('Execution', () => {
|
|||
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
|
||||
cy.intercept('POST', '/rest/workflows/**/run?**').as('workflowRun');
|
||||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('do something with them')
|
||||
|
@ -525,7 +525,7 @@ describe('Execution', () => {
|
|||
|
||||
workflowPage.getters.zoomToFitButton().click();
|
||||
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
|
||||
cy.intercept('POST', '/rest/workflows/**/run?**').as('workflowRun');
|
||||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('If')
|
||||
|
@ -547,7 +547,7 @@ describe('Execution', () => {
|
|||
|
||||
workflowPage.getters.clearExecutionDataButton().should('be.visible');
|
||||
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('workflowRun');
|
||||
cy.intercept('POST', '/rest/workflows/**/run?**').as('workflowRun');
|
||||
|
||||
workflowPage.getters
|
||||
.canvasNodeByName('NoOp2')
|
||||
|
@ -576,7 +576,7 @@ describe('Execution', () => {
|
|||
it('should successfully execute partial executions with nodes attached to the second output', () => {
|
||||
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.executeWorkflowButton().click();
|
||||
|
@ -596,7 +596,7 @@ describe('Execution', () => {
|
|||
it('should execute workflow partially up to the node that has issues', () => {
|
||||
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.executeWorkflowButton().click();
|
||||
|
@ -616,4 +616,45 @@ describe('Execution', () => {
|
|||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { type ICredentialType } from 'n8n-workflow';
|
||||
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
AI_TOOL_HTTP_NODE_NAME,
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
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 { WorkflowExecutionsTab } from '../pages/workflow-executions-tab';
|
||||
import executionOutOfMemoryServerResponse from '../fixtures/responses/execution-out-of-memory-server-response.json';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
@ -9,223 +10,252 @@ const executionsTab = new WorkflowExecutionsTab();
|
|||
const executionsRefreshInterval = 4000;
|
||||
|
||||
// Test suite for executions tab
|
||||
describe('Current Workflow Executions', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
|
||||
});
|
||||
|
||||
it('should render executions tab correctly', () => {
|
||||
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().should('have.length', 11);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 2);
|
||||
executionsTab.getters
|
||||
.executionListItems()
|
||||
.first()
|
||||
.invoke('attr', 'class')
|
||||
.should('match', /_active_/);
|
||||
});
|
||||
|
||||
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/active?filter=*');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.wait(1000);
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
});
|
||||
|
||||
it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
|
||||
const throttleResponse: RouteHandler = async (req) => {
|
||||
return await new Promise((resolve) => {
|
||||
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);
|
||||
describe('Workflow Executions', () => {
|
||||
describe('when workflow is saved', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', 'My test workflow');
|
||||
});
|
||||
|
||||
cy.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-reset-button').should('be.visible').click();
|
||||
executionsTab.getters.executionListItems().eq(11).should('be.visible');
|
||||
it('should render executions tab correctly', () => {
|
||||
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().should('have.length', 11);
|
||||
executionsTab.getters.successfulExecutionListItems().should('have.length', 9);
|
||||
executionsTab.getters.failedExecutionListItems().should('have.length', 2);
|
||||
executionsTab.getters
|
||||
.executionListItems()
|
||||
.first()
|
||||
.invoke('attr', 'class')
|
||||
.should('match', /_active_/);
|
||||
});
|
||||
|
||||
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/active?filter=*');
|
||||
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
cy.wait(1000);
|
||||
executionsTab.actions.switchToEditorTab();
|
||||
cy.wait(executionsRefreshInterval);
|
||||
cy.url().should('not.include', '/executions');
|
||||
});
|
||||
|
||||
it('should not redirect back to execution tab when slow request is not done before leaving the page', () => {
|
||||
const throttleResponse: RouteHandler = async (req) => {
|
||||
return await new Promise((resolve) => {
|
||||
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.getByTestId('executions-filter-button').click();
|
||||
cy.getByTestId('executions-filter-reset-button').should('be.visible').click();
|
||||
executionsTab.getters.executionListItems().eq(11).should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when new workflow is not saved', () => {
|
||||
beforeEach(() => {
|
||||
workflowPage.actions.visit();
|
||||
});
|
||||
|
||||
it('should open executions tab', () => {
|
||||
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();
|
||||
|
||||
cy.getByTestId('node-creator-item-name')
|
||||
.should('be.visible')
|
||||
.filter(':contains("Trigger")')
|
||||
.click();
|
||||
executionsTab.actions.switchToExecutionsTab();
|
||||
executionsTab.getters.executionsSidebar().should('be.visible');
|
||||
executionsTab.getters.executionsEmptyList().should('be.visible');
|
||||
cy.getByTestId('workflow-execution-no-content').should('be.visible');
|
||||
|
||||
workflowPage.getters.saveButton().find('button').should('be.enabled').click();
|
||||
workflowPage.getters.isWorkflowSaved();
|
||||
workflowPage.getters.nodeViewRoot().should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -233,9 +263,11 @@ const createMockExecutions = () => {
|
|||
executionsTab.actions.createManualExecutions(5);
|
||||
// Make some failed executions by enabling Code node with syntax error
|
||||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
workflowPage.getters.disabledNodes().should('have.length', 0);
|
||||
executionsTab.actions.createManualExecutions(2);
|
||||
// Then add some more successful ones
|
||||
executionsTab.actions.toggleNodeEnabled('Error');
|
||||
workflowPage.getters.disabledNodes().should('have.length', 1);
|
||||
executionsTab.actions.createManualExecutions(4);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
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 { getVisibleSelect } from '../utils';
|
||||
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,
|
||||
|
@ -13,6 +13,7 @@ import {
|
|||
installFirstCommunityNode,
|
||||
visitCommunityNodesSettings,
|
||||
} from '../pages/settings-community-nodes';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const credentialsModal = new CredentialsModal();
|
||||
const nodeCreatorFeature = new NodeCreator();
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
import type { ExecutionError } from 'n8n-workflow/src';
|
||||
import { NDV, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
|
||||
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,
|
||||
|
@ -18,19 +31,7 @@ import {
|
|||
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
} from '../constants';
|
||||
import {
|
||||
clickCreateNewCredential,
|
||||
clickExecuteNode,
|
||||
clickGetBackToCanvas,
|
||||
} from '../composables/ndv';
|
||||
import { setCredentialValues } from '../composables/modals/credential-modal';
|
||||
import {
|
||||
closeManualChatModal,
|
||||
getManualChatMessages,
|
||||
getManualChatModalLogs,
|
||||
getManualChatModalLogsEntries,
|
||||
sendManualChatMessage,
|
||||
} from '../composables/modals/chat-modal';
|
||||
import { NDV, WorkflowPage as WorkflowPageClass } from '../pages';
|
||||
import { createMockNodeExecutionData, getVisibleSelect, runMockWorkflowExecution } from '../utils';
|
||||
|
||||
const ndv = new NDV();
|
||||
|
|
|
@ -227,7 +227,7 @@ describe('NDV', () => {
|
|||
|
||||
workflowPage.actions.zoomToFit();
|
||||
|
||||
/* prettier-ignore */
|
||||
// biome-ignore format:
|
||||
const PINNED_DATA = [
|
||||
{
|
||||
"id": "abc",
|
||||
|
@ -263,7 +263,6 @@ describe('NDV', () => {
|
|||
]
|
||||
}
|
||||
];
|
||||
/* prettier-ignore */
|
||||
workflowPage.actions.openNode('Get thread details1');
|
||||
ndv.actions.pastePinnedData(PINNED_DATA);
|
||||
ndv.actions.close();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import planData from '../fixtures/Plan_data_opt_in_trial.json';
|
||||
import {
|
||||
BannerStack,
|
||||
MainSidebar,
|
||||
|
@ -5,7 +6,6 @@ import {
|
|||
visitPublicApiPage,
|
||||
getPublicApiUpgradeCTA,
|
||||
} from '../pages';
|
||||
import planData from '../fixtures/Plan_data_opt_in_trial.json';
|
||||
|
||||
const mainSidebar = new MainSidebar();
|
||||
const bannerStack = new BannerStack();
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import generateOTPToken from 'cypress-otp';
|
||||
|
||||
import { MainSidebar } from './../pages/sidebar/main-sidebar';
|
||||
import { INSTANCE_OWNER, INSTANCE_ADMIN, BACKEND_BASE_URL } from '../constants';
|
||||
import { SigninPage } from '../pages';
|
||||
import { PersonalSettingsPage } from '../pages/settings-personal';
|
||||
import { MfaLoginPage } from '../pages/mfa-login';
|
||||
import { MainSidebar } from './../pages/sidebar/main-sidebar';
|
||||
import { PersonalSettingsPage } from '../pages/settings-personal';
|
||||
|
||||
const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD';
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ describe('Debug', () => {
|
|||
it('should be able to debug executions', () => {
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
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();
|
||||
|
||||
|
|
|
@ -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 { WorkflowPage } from '../pages/workflow';
|
||||
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 workflowPage = new WorkflowPage();
|
||||
|
|
|
@ -142,7 +142,7 @@ describe('Editor actions should work', () => {
|
|||
it('after switching between Editor and Debug', () => {
|
||||
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
|
||||
cy.intercept('GET', '/rest/executions/*').as('getExecution');
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun');
|
||||
cy.intercept('POST', '/rest/workflows/**/run?**').as('postWorkflowRun');
|
||||
|
||||
editWorkflowAndDeactivate();
|
||||
workflowPage.actions.executeWorkflow();
|
||||
|
|
|
@ -1,4 +1,34 @@
|
|||
import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils';
|
||||
import {
|
||||
AGENT_NODE_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
} 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,
|
||||
|
@ -14,37 +44,7 @@ import {
|
|||
openNode,
|
||||
getConnectionBySourceAndTarget,
|
||||
} 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 {
|
||||
AGENT_NODE_NAME,
|
||||
MANUAL_CHAT_TRIGGER_NODE_NAME,
|
||||
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
|
||||
MANUAL_TRIGGER_NODE_NAME,
|
||||
AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME,
|
||||
AI_TOOL_CALCULATOR_NODE_NAME,
|
||||
AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME,
|
||||
AI_TOOL_CODE_NODE_NAME,
|
||||
AI_TOOL_WIKIPEDIA_NODE_NAME,
|
||||
BASIC_LLM_CHAIN_NODE_NAME,
|
||||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
CHAT_TRIGGER_NODE_DISPLAY_NAME,
|
||||
} from './../constants';
|
||||
import { createMockNodeExecutionData, runMockWorkflowExecution } from '../utils';
|
||||
|
||||
describe('Langchain Integration', () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import workflow from '../fixtures/Manual_wait_set.json';
|
||||
import { importWorkflow, visitDemoPage } from '../pages/demo';
|
||||
import { WorkflowPage } from '../pages/workflow';
|
||||
import { errorToast } from '../pages/notifications';
|
||||
import { WorkflowPage } from '../pages/workflow';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ describe('Personal Settings', () => {
|
|||
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('save-settings-button').click();
|
||||
errorToast().should('contain', 'Potentially malicious string | Potentially malicious string');
|
||||
errorToast().should('contain', 'Potentially malicious string');
|
||||
errorToast().find('.el-notification__closeBtn').click();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
clickUseWorkflowButtonByTitle,
|
||||
visitTemplateCollectionPage,
|
||||
|
@ -5,11 +10,6 @@ import {
|
|||
} from '../pages/template-collection';
|
||||
import * as templateCredentialsSetupPage from '../pages/template-credential-setup';
|
||||
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();
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { WorkflowsPage } from '../pages/workflows';
|
||||
import {
|
||||
closeVersionUpdatesPanel,
|
||||
getVersionCard,
|
||||
getVersionUpdatesPanelOpenButton,
|
||||
openVersionUpdatesPanel,
|
||||
} from '../composables/versions';
|
||||
import { WorkflowsPage } from '../pages/workflows';
|
||||
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import * as projects from '../composables/projects';
|
||||
import { INSTANCE_MEMBERS, MANUAL_TRIGGER_NODE_NAME, NOTION_NODE_NAME } from '../constants';
|
||||
import {
|
||||
WorkflowsPage,
|
||||
|
@ -8,7 +9,6 @@ import {
|
|||
NDV,
|
||||
MainSidebar,
|
||||
} from '../pages';
|
||||
import * as projects from '../composables/projects';
|
||||
import { getVisibleDropdown, getVisibleModalOverlay, getVisibleSelect } from '../utils';
|
||||
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { NodeCreator } from '../pages/features/node-creator';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { NDV } from '../pages/ndv';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
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 WorkflowPage = new WorkflowPageClass();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { EDIT_FIELDS_SET_NODE_NAME } from '../constants';
|
||||
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();
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import { NDV, WorkflowPage } from '../pages';
|
||||
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(() => {
|
||||
|
@ -31,7 +38,8 @@ describe('AI Assistant::enabled', () => {
|
|||
aiAssistant.getters.askAssistantFloatingButton().click();
|
||||
aiAssistant.getters.askAssistantChat().should('be.visible');
|
||||
aiAssistant.getters.placeholderMessage().should('be.visible');
|
||||
aiAssistant.getters.chatInputWrapper().should('not.exist');
|
||||
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');
|
||||
|
@ -130,17 +138,24 @@ describe('AI Assistant::enabled', () => {
|
|||
ndv.getters.nodeExecuteButton().click();
|
||||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.quickReplies().should('have.length', 2);
|
||||
aiAssistant.getters.quickReplies().eq(0).click();
|
||||
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 send message to assistant when node is executed', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/simple_message_response.json',
|
||||
it('should show quick replies when node is executed after new suggestion', () => {
|
||||
cy.intercept('POST', '/rest/ai-assistant/chat', (req) => {
|
||||
req.reply((res) => {
|
||||
if (['init-error-helper', 'message'].includes(req.body.payload.type)) {
|
||||
res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' });
|
||||
} else if (req.body.payload.type === 'event') {
|
||||
res.send({ statusCode: 200, fixture: 'aiAssistant/node_execution_error_response.json' });
|
||||
} else {
|
||||
res.send({ statusCode: 500 });
|
||||
}
|
||||
});
|
||||
}).as('chatRequest');
|
||||
cy.createFixtureWorkflow('aiAssistant/test_workflow.json');
|
||||
wf.actions.openNode('Edit Fields');
|
||||
|
@ -148,10 +163,17 @@ describe('AI Assistant::enabled', () => {
|
|||
aiAssistant.getters.nodeErrorViewAssistantButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesAssistant().should('have.length', 1);
|
||||
// Executing the same node should sende a new message to the assistant automatically
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
cy.wait('@chatRequest');
|
||||
aiAssistant.getters.chatMessagesAssistant().should('have.length', 2);
|
||||
// Respond 'Yes' to the quick reply (request new suggestion)
|
||||
aiAssistant.getters.quickReplies().contains('Yes').click();
|
||||
cy.wait('@chatRequest');
|
||||
// No quick replies at this point
|
||||
aiAssistant.getters.quickReplies().should('not.exist');
|
||||
ndv.getters.nodeExecuteButton().click();
|
||||
// But after executing the node again, quick replies should be shown
|
||||
aiAssistant.getters.chatMessagesAssistant().should('have.length', 4);
|
||||
aiAssistant.getters.quickReplies().should('have.length', 2);
|
||||
});
|
||||
|
||||
it('should warn before starting a new session', () => {
|
||||
|
@ -244,4 +266,230 @@ describe('AI Assistant::enabled', () => {
|
|||
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-assistant/chat', (req) => {
|
||||
req.reply((res) => {
|
||||
if (['init-support-chat'].includes(req.body.payload.type)) {
|
||||
res.send({ statusCode: 200, fixture: 'aiAssistant/simple_message_response.json' });
|
||||
} else {
|
||||
res.send({ statusCode: 200, fixture: 'aiAssistant/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-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/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-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/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-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/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-assistant/chat', {
|
||||
statusCode: 200,
|
||||
fixture: 'aiAssistant/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}}');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { setCredentialValues } from '../composables/modals/credential-modal';
|
||||
import { clickCreateNewCredential } from '../composables/ndv';
|
||||
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, NOTION_NODE_NAME } from '../constants';
|
||||
import { NDV, WorkflowPage } from '../pages';
|
||||
import { NodeCreator } from '../pages/features/node-creator';
|
||||
import { clickCreateNewCredential } from '../composables/ndv';
|
||||
import { setCredentialValues } from '../composables/modals/credential-modal';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
@ -132,6 +132,10 @@ describe('NDV', () => {
|
|||
'contains.text',
|
||||
"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');
|
||||
// The error details should be hidden behind a tooltip
|
||||
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Start Time');
|
||||
ndv.getters.nodeRunErrorIndicator().should('not.contain', 'Execution Time');
|
||||
});
|
||||
|
||||
it('should save workflow using keyboard shortcut from NDV', () => {
|
||||
|
@ -582,7 +586,13 @@ describe('NDV', () => {
|
|||
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).click();
|
||||
ndv.getters
|
||||
.outputDisplayMode()
|
||||
.find('label')
|
||||
.eq(1)
|
||||
.scrollIntoView()
|
||||
.should('be.visible')
|
||||
.click();
|
||||
|
||||
ndv.getters.outputDataContainer().find('.json-data').should('exist');
|
||||
ndv.getters
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
import { NDV } from '../pages/ndv';
|
||||
import { successToast } from '../pages/notifications';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
const ndv = new NDV();
|
||||
|
@ -38,6 +39,55 @@ describe('Code node', () => {
|
|||
|
||||
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', () => {
|
||||
|
|
|
@ -5,11 +5,11 @@ import {
|
|||
EDIT_FIELDS_SET_NODE_NAME,
|
||||
NOTION_NODE_NAME,
|
||||
} from '../constants';
|
||||
import { WorkflowExecutionsTab } from '../pages';
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { WorkflowExecutionsTab } from '../pages';
|
||||
import { errorToast, successToast } from '../pages/notifications';
|
||||
|
||||
const NEW_WORKFLOW_NAME = 'Something else';
|
||||
const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow';
|
||||
|
|
133
cypress/fixtures/Execution-pinned-data-check.json
Normal file
133
cypress/fixtures/Execution-pinned-data-check.json
Normal 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": []
|
||||
}
|
28
cypress/fixtures/aiAssistant/code_snippet_response.json
Normal file
28
cypress/fixtures/aiAssistant/code_snippet_response.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"sessionId": "f9130bd7-c078-4862-a38a-369b27b0ff20-e96eb9f7-d581-4684-b6a9-fd3dfe9fe1fb-XCldJLlusGrEVku5I9cYT",
|
||||
"sessionId": "1",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"type": "agent-suggestion",
|
||||
"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!"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -7,25 +7,27 @@
|
|||
"test:e2e:ui": "scripts/run-e2e.js ui",
|
||||
"test:e2e:dev": "scripts/run-e2e.js dev",
|
||||
"test:e2e:all": "scripts/run-e2e.js all",
|
||||
"format": "prettier --write . --ignore-path ../.prettierignore",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint . --quiet",
|
||||
"lintfix": "eslint . --fix",
|
||||
"develop": "cd ..; pnpm dev",
|
||||
"start": "cd ..; pnpm start"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/api-types": "workspace:*",
|
||||
"@types/lodash": "catalog:",
|
||||
"eslint-plugin-cypress": "^3.3.0",
|
||||
"eslint-plugin-cypress": "^3.5.0",
|
||||
"n8n-workflow": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ngneat/falso": "^7.2.0",
|
||||
"@sinonjs/fake-timers": "^11.2.2",
|
||||
"cypress": "^13.11.0",
|
||||
"@sinonjs/fake-timers": "^13.0.2",
|
||||
"cypress": "^13.14.2",
|
||||
"cypress-otp": "^1.0.3",
|
||||
"cypress-real-events": "^1.12.0",
|
||||
"cypress-real-events": "^1.13.0",
|
||||
"lodash": "catalog:",
|
||||
"nanoid": "catalog:",
|
||||
"start-server-and-test": "^2.0.5"
|
||||
"start-server-and-test": "^2.0.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ export class AIAssistant extends BasePage {
|
|||
chatMessagesAssistant: () => cy.getByTestId('chat-message-assistant'),
|
||||
chatMessagesUser: () => cy.getByTestId('chat-message-user'),
|
||||
chatMessagesSystem: () => cy.getByTestId('chat-message-system'),
|
||||
quickReplies: () => cy.getByTestId('quick-replies').find('button'),
|
||||
quickReplies: () => cy.getByTestId('quick-replies'),
|
||||
quickReplyButtons: () => this.getters.quickReplies().find('button'),
|
||||
newAssistantSessionModal: () => cy.getByTestId('new-assistant-session-modal'),
|
||||
codeDiffs: () => cy.getByTestId('code-diff-suggestion'),
|
||||
applyCodeDiffButtons: () => cy.getByTestId('replace-code-button'),
|
||||
|
@ -34,16 +35,29 @@ export class AIAssistant extends BasePage {
|
|||
codeReplacedMessage: () => cy.getByTestId('code-replaced-message'),
|
||||
nodeErrorViewAssistantButton: () =>
|
||||
cy.getByTestId('node-error-view-ask-assistant-button').find('button').first(),
|
||||
credentialEditAssistantButton: () => cy.getByTestId('credential-edit-ask-assistant-button'),
|
||||
codeSnippet: () => cy.getByTestId('assistant-code-snippet-content'),
|
||||
};
|
||||
|
||||
actions = {
|
||||
enableAssistant(): void {
|
||||
enableAssistant: () => {
|
||||
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.enabledFor);
|
||||
cy.enableFeature(AI_ASSISTANT_FEATURE.name);
|
||||
},
|
||||
disableAssistant(): void {
|
||||
disableAssistant: () => {
|
||||
overrideFeatureFlag(AI_ASSISTANT_FEATURE.experimentName, AI_ASSISTANT_FEATURE.disabledFor);
|
||||
cy.disableFeature(AI_ASSISTANT_FEATURE.name);
|
||||
},
|
||||
sendMessage: (message: string) => {
|
||||
this.getters.chatInput().type(message).type('{enter}');
|
||||
},
|
||||
closeChat: () => {
|
||||
this.getters.closeChatButton().click();
|
||||
this.getters.askAssistantChat().should('not.be.visible');
|
||||
},
|
||||
openChat: () => {
|
||||
this.getters.askAssistantFloatingButton().click();
|
||||
this.getters.askAssistantChat().should('be.visible');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { N8N_AUTH_COOKIE } from '../constants';
|
||||
import { BasePage } from './base';
|
||||
import { SigninPage } from './signin';
|
||||
import { WorkflowsPage } from './workflows';
|
||||
import { N8N_AUTH_COOKIE } from '../constants';
|
||||
|
||||
export class MfaLoginPage extends BasePage {
|
||||
url = '/mfa';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { BasePage } from '../base';
|
||||
import { getVisibleSelect } from '../../utils';
|
||||
import { BasePage } from '../base';
|
||||
|
||||
export class CredentialsModal extends BasePage {
|
||||
getters = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { getVisiblePopper, getVisibleSelect } from '../utils';
|
||||
import { BasePage } from './base';
|
||||
import { getVisiblePopper, getVisibleSelect } from '../utils';
|
||||
|
||||
export class NDV extends BasePage {
|
||||
getters = {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { getVisibleSelect } from '../utils';
|
||||
import { BasePage } from './base';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
export class SettingsLogStreamingPage extends BasePage {
|
||||
url = '/settings/log-streaming';
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import generateOTPToken from 'cypress-otp';
|
||||
|
||||
import { BasePage } from './base';
|
||||
import { ChangePasswordModal } from './modals/change-password-modal';
|
||||
import { MfaSetupModal } from './modals/mfa-setup-modal';
|
||||
import { BasePage } from './base';
|
||||
|
||||
const changePasswordModal = new ChangePasswordModal();
|
||||
const mfaSetupModal = new MfaSetupModal();
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { SettingsSidebar } from './sidebar/settings-sidebar';
|
||||
import { BasePage } from './base';
|
||||
import { MainSidebar } from './sidebar/main-sidebar';
|
||||
import { SettingsSidebar } from './sidebar/settings-sidebar';
|
||||
import { WorkflowPage } from './workflow';
|
||||
import { WorkflowsPage } from './workflows';
|
||||
import { BasePage } from './base';
|
||||
|
||||
const workflowPage = new WorkflowPage();
|
||||
const workflowsPage = new WorkflowsPage();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { N8N_AUTH_COOKIE } from '../constants';
|
||||
import { BasePage } from './base';
|
||||
import { WorkflowsPage } from './workflows';
|
||||
import { N8N_AUTH_COOKIE } from '../constants';
|
||||
|
||||
export class SigninPage extends BasePage {
|
||||
url = '/signin';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as formStep from '../composables/setup-template-form-step';
|
||||
import { overrideFeatureFlag } from '../composables/featureFlags';
|
||||
import { CredentialsModal, MessageBox } from './modals';
|
||||
import { overrideFeatureFlag } from '../composables/featureFlags';
|
||||
import * as formStep from '../composables/setup-template-form-step';
|
||||
|
||||
const credentialsModal = new CredentialsModal();
|
||||
const messageBox = new MessageBox();
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { BasePage } from './base';
|
||||
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
export class VariablesPage extends BasePage {
|
||||
|
|
|
@ -7,6 +7,7 @@ export class WorkflowExecutionsTab extends BasePage {
|
|||
getters = {
|
||||
executionsTabButton: () => cy.getByTestId('radio-button-executions'),
|
||||
executionsSidebar: () => cy.getByTestId('executions-sidebar'),
|
||||
executionsEmptyList: () => cy.getByTestId('execution-list-empty'),
|
||||
autoRefreshCheckBox: () => cy.getByTestId('auto-refresh-checkbox'),
|
||||
executionsList: () => cy.getByTestId('current-executions-list'),
|
||||
executionListItems: () => this.getters.executionsList().find('div.execution-card'),
|
||||
|
@ -34,7 +35,7 @@ export class WorkflowExecutionsTab extends BasePage {
|
|||
},
|
||||
createManualExecutions: (count: number) => {
|
||||
for (let i = 0; i < count; i++) {
|
||||
cy.intercept('POST', '/rest/workflows/**/run').as('workflowExecution');
|
||||
cy.intercept('POST', '/rest/workflows/**/run?**').as('workflowExecution');
|
||||
workflowPage.actions.executeWorkflow();
|
||||
cy.wait('@workflowExecution');
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { BasePage } from './base';
|
||||
import { NodeCreator } from './features/node-creator';
|
||||
import { META_KEY } from '../constants';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
import { getUniqueWorkflowName } from '../utils/workflowUtils';
|
||||
import { BasePage } from './base';
|
||||
import { NodeCreator } from './features/node-creator';
|
||||
|
||||
const nodeCreator = new NodeCreator();
|
||||
export class WorkflowPage extends BasePage {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import 'cypress-real-events';
|
||||
import type { FrontendSettings } from '@n8n/api-types';
|
||||
import FakeTimers from '@sinonjs/fake-timers';
|
||||
import type { IN8nUISettings } from 'n8n-workflow';
|
||||
import { WorkflowPage } from '../pages';
|
||||
|
||||
import {
|
||||
BACKEND_BASE_URL,
|
||||
INSTANCE_ADMIN,
|
||||
|
@ -9,6 +9,7 @@ import {
|
|||
INSTANCE_OWNER,
|
||||
N8N_AUTH_COOKIE,
|
||||
} from '../constants';
|
||||
import { WorkflowPage } from '../pages';
|
||||
import { getUniqueWorkflowName } from '../utils/workflowUtils';
|
||||
|
||||
Cypress.Commands.add('setAppDate', (targetDate: number | Date) => {
|
||||
|
@ -86,8 +87,8 @@ Cypress.Commands.add('signout', () => {
|
|||
cy.getCookie(N8N_AUTH_COOKIE).should('not.exist');
|
||||
});
|
||||
|
||||
export let settings: Partial<IN8nUISettings>;
|
||||
Cypress.Commands.add('overrideSettings', (value: Partial<IN8nUISettings>) => {
|
||||
export let settings: Partial<FrontendSettings>;
|
||||
Cypress.Commands.add('overrideSettings', (value: Partial<FrontendSettings>) => {
|
||||
settings = value;
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import merge from 'lodash/merge';
|
||||
|
||||
import { settings } from './commands';
|
||||
|
||||
before(() => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Load type definitions that come with Cypress module
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import type { IN8nUISettings } from 'n8n-workflow';
|
||||
import type { FrontendSettings } from '@n8n/api-types';
|
||||
|
||||
Cypress.Keyboard.defaults({
|
||||
keystrokeDelay: 0,
|
||||
|
@ -45,7 +45,7 @@ declare global {
|
|||
*/
|
||||
signinAsMember(index?: number): void;
|
||||
signout(): void;
|
||||
overrideSettings(value: Partial<IN8nUISettings>): void;
|
||||
overrideSettings(value: Partial<FrontendSettings>): void;
|
||||
enableFeature(feature: string): void;
|
||||
disableFeature(feature: string): void;
|
||||
enableQueueMode(): void;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import type { IDataObject, IPinData, ITaskData, ITaskDataConnections } from 'n8n-workflow';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { clickExecuteWorkflowButton } from '../composables/workflow';
|
||||
|
||||
export function createMockNodeExecutionData(
|
||||
|
@ -88,7 +89,7 @@ export function runMockWorkflowExecution({
|
|||
}) {
|
||||
const executionId = nanoid(8);
|
||||
|
||||
cy.intercept('POST', '/rest/workflows/**/run', {
|
||||
cy.intercept('POST', '/rest/workflows/**/run?**', {
|
||||
statusCode: 201,
|
||||
body: {
|
||||
data: {
|
||||
|
|
|
@ -6,7 +6,7 @@ FROM --platform=linux/amd64 n8nio/base:${NODE_VERSION} AS builder
|
|||
# Build the application from source
|
||||
WORKDIR /src
|
||||
COPY . /src
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store --mount=type=cache,id=pnpm-metadata,target=/root/.cache/pnpm/metadata pnpm install --frozen-lockfile
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store --mount=type=cache,id=pnpm-metadata,target=/root/.cache/pnpm/metadata DOCKER_BUILD=true pnpm install --frozen-lockfile
|
||||
RUN pnpm build
|
||||
|
||||
# Delete all dev dependencies
|
||||
|
@ -18,7 +18,7 @@ RUN find . -type f -name "*.ts" -o -name "*.js.map" -o -name "*.vue" -o -name "t
|
|||
|
||||
# Deploy the `n8n` package into /compiled
|
||||
RUN mkdir /compiled
|
||||
RUN NODE_ENV=production pnpm --filter=n8n --prod --no-optional deploy /compiled
|
||||
RUN NODE_ENV=production DOCKER_BUILD=true pnpm --filter=n8n --prod --no-optional deploy /compiled
|
||||
|
||||
# 2. Start with a new clean image with just the code that is needed to run n8n
|
||||
FROM n8nio/base:${NODE_VERSION}
|
||||
|
|
16
lefthook.yml
Normal file
16
lefthook.yml
Normal file
|
@ -0,0 +1,16 @@
|
|||
pre-commit:
|
||||
commands:
|
||||
biome_check:
|
||||
glob: 'packages/**/*.{js,ts,json}'
|
||||
run: ./node_modules/.bin/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
||||
stage_fixed: true
|
||||
skip:
|
||||
- merge
|
||||
- rebase
|
||||
prettier_check:
|
||||
glob: 'packages/**/*.{vue,yml,md}'
|
||||
run: ./node_modules/.bin/prettier --write --ignore-unknown --no-error-on-unmatched-pattern {staged_files}
|
||||
stage_fixed: true
|
||||
skip:
|
||||
- merge
|
||||
- rebase
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".",
|
||||
},
|
||||
],
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
15
package.json
15
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "n8n-monorepo",
|
||||
"version": "1.57.0",
|
||||
"version": "1.60.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20.15",
|
||||
|
@ -8,6 +8,7 @@
|
|||
},
|
||||
"packageManager": "pnpm@9.6.0",
|
||||
"scripts": {
|
||||
"prepare": "node scripts/prepare.mjs",
|
||||
"preinstall": "node scripts/block-npm-install.js",
|
||||
"build": "turbo run build",
|
||||
"build:backend": "turbo run build:backend",
|
||||
|
@ -17,7 +18,9 @@
|
|||
"dev": "turbo run dev --parallel --env-mode=loose --filter=!n8n-design-system --filter=!@n8n/chat",
|
||||
"dev:ai": "turbo run dev --parallel --env-mode=loose --filter=@n8n/nodes-langchain --filter=n8n --filter=n8n-core",
|
||||
"clean": "turbo run clean --parallel",
|
||||
"reset": "node scripts/ensure-zx.mjs && zx scripts/reset.mjs",
|
||||
"format": "turbo run format && node scripts/format.mjs",
|
||||
"format:check": "turbo run format:check",
|
||||
"lint": "turbo run lint",
|
||||
"lintfix": "turbo run lintfix",
|
||||
"lint:backend": "turbo run lint:backend",
|
||||
|
@ -37,6 +40,7 @@
|
|||
"worker": "./packages/cli/bin/n8n worker"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.0",
|
||||
"@n8n_io/eslint-config": "workspace:*",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/supertest": "^6.0.2",
|
||||
|
@ -45,6 +49,7 @@
|
|||
"jest-expect-message": "^1.1.3",
|
||||
"jest-mock": "^29.6.2",
|
||||
"jest-mock-extended": "^3.0.4",
|
||||
"lefthook": "^1.7.15",
|
||||
"nock": "^13.3.2",
|
||||
"nodemon": "^3.0.1",
|
||||
"p-limit": "^3.1.0",
|
||||
|
@ -55,7 +60,8 @@
|
|||
"tsc-alias": "^1.8.7",
|
||||
"tsc-watch": "^6.0.4",
|
||||
"turbo": "2.0.6",
|
||||
"typescript": "*"
|
||||
"typescript": "*",
|
||||
"zx": "^8.1.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
|
@ -66,17 +72,16 @@
|
|||
"chokidar": "3.5.2",
|
||||
"esbuild": "^0.20.2",
|
||||
"formidable": "3.5.1",
|
||||
"prettier": "^3.2.5",
|
||||
"pug": "^3.0.3",
|
||||
"semver": "^7.5.4",
|
||||
"tslib": "^2.6.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.5.2",
|
||||
"typescript": "^5.6.2",
|
||||
"ws": ">=8.17.1"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"typedi@0.10.0": "patches/typedi@0.10.0.patch",
|
||||
"@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch",
|
||||
"@sentry/cli@2.36.2": "patches/@sentry__cli@2.36.2.patch",
|
||||
"pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch",
|
||||
"pyodide@0.23.4": "patches/pyodide@0.23.4.patch",
|
||||
"@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch",
|
||||
|
|
7
packages/@n8n/api-types/.eslintrc.js
Normal file
7
packages/@n8n/api-types/.eslintrc.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
const sharedOptions = require('@n8n_io/eslint-config/shared');
|
||||
|
||||
/** @type {import('@types/eslint').ESLint.ConfigData} */
|
||||
module.exports = {
|
||||
extends: ['@n8n_io/eslint-config/base'],
|
||||
...sharedOptions(__dirname),
|
||||
};
|
3
packages/@n8n/api-types/README.md
Normal file
3
packages/@n8n/api-types/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
## @n8n/api-types
|
||||
|
||||
This package contains types and schema definitions for the n8n internal API, so that these can be shared between the backend and the frontend code.
|
2
packages/@n8n/api-types/jest.config.js
Normal file
2
packages/@n8n/api-types/jest.config.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
/** @type {import('jest').Config} */
|
||||
module.exports = require('../../../jest.config');
|
31
packages/@n8n/api-types/package.json
Normal file
31
packages/@n8n/api-types/package.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "@n8n/api-types",
|
||||
"version": "0.2.0",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist .turbo",
|
||||
"dev": "pnpm watch",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"format": "biome format --write .",
|
||||
"format:check": "biome ci .",
|
||||
"lint": "eslint .",
|
||||
"lintfix": "eslint . --fix",
|
||||
"watch": "tsc -p tsconfig.build.json --watch",
|
||||
"test": "jest",
|
||||
"test:dev": "jest --watch"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "src/index.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"n8n-workflow": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"xss": "catalog:",
|
||||
"zod": "catalog:",
|
||||
"zod-class": "0.0.15"
|
||||
}
|
||||
}
|
2
packages/@n8n/api-types/src/datetime.ts
Normal file
2
packages/@n8n/api-types/src/datetime.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
/** Date time in the ISO 8601 format, e.g. 2024-10-31T00:00:00.123Z */
|
||||
export type Iso8601DateTimeString = string;
|
4
packages/@n8n/api-types/src/dto/index.ts
Normal file
4
packages/@n8n/api-types/src/dto/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export { PasswordUpdateRequestDto } from './user/password-update-request.dto';
|
||||
export { RoleChangeRequestDto } from './user/role-change-request.dto';
|
||||
export { SettingsUpdateRequestDto } from './user/settings-update-request.dto';
|
||||
export { UserUpdateRequestDto } from './user/user-update-request.dto';
|
|
@ -0,0 +1,50 @@
|
|||
import { PasswordUpdateRequestDto } from '../password-update-request.dto';
|
||||
|
||||
describe('PasswordUpdateRequestDto', () => {
|
||||
it('should fail validation with missing currentPassword', () => {
|
||||
const data = {
|
||||
newPassword: 'newPassword123',
|
||||
mfaCode: '123456',
|
||||
};
|
||||
|
||||
const result = PasswordUpdateRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe('currentPassword');
|
||||
});
|
||||
|
||||
it('should fail validation with missing newPassword', () => {
|
||||
const data = {
|
||||
currentPassword: 'oldPassword123',
|
||||
mfaCode: '123456',
|
||||
};
|
||||
|
||||
const result = PasswordUpdateRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe('newPassword');
|
||||
});
|
||||
|
||||
it('should pass validation with missing mfaCode', () => {
|
||||
const data = {
|
||||
currentPassword: 'oldPassword123',
|
||||
newPassword: 'newPassword123',
|
||||
};
|
||||
|
||||
const result = PasswordUpdateRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass validation with valid data', () => {
|
||||
const data = {
|
||||
currentPassword: 'oldPassword123',
|
||||
newPassword: 'newPassword123',
|
||||
mfaCode: '123456',
|
||||
};
|
||||
|
||||
const result = PasswordUpdateRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import { RoleChangeRequestDto } from '../role-change-request.dto';
|
||||
|
||||
describe('RoleChangeRequestDto', () => {
|
||||
it('should fail validation with missing newRoleName', () => {
|
||||
const data = {};
|
||||
|
||||
const result = RoleChangeRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe('newRoleName');
|
||||
expect(result.error?.issues[0].message).toBe('New role is required');
|
||||
});
|
||||
|
||||
it('should fail validation with invalid newRoleName', () => {
|
||||
const data = {
|
||||
newRoleName: 'invalidRole',
|
||||
};
|
||||
|
||||
const result = RoleChangeRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe('newRoleName');
|
||||
expect(result.error?.issues[0].message).toBe(
|
||||
"Invalid enum value. Expected 'global:admin' | 'global:member', received 'invalidRole'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass validation with valid data', () => {
|
||||
const data = {
|
||||
newRoleName: 'global:admin',
|
||||
};
|
||||
|
||||
const result = RoleChangeRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,68 @@
|
|||
import { SettingsUpdateRequestDto } from '../settings-update-request.dto';
|
||||
|
||||
describe('SettingsUpdateRequestDto', () => {
|
||||
it('should pass validation with missing userActivated', () => {
|
||||
const data = {
|
||||
allowSSOManualLogin: false,
|
||||
};
|
||||
|
||||
const result = SettingsUpdateRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass validation with missing allowSSOManualLogin', () => {
|
||||
const data = {
|
||||
userActivated: true,
|
||||
};
|
||||
|
||||
const result = SettingsUpdateRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass validation with missing userActivated and allowSSOManualLogin', () => {
|
||||
const data = {};
|
||||
|
||||
const result = SettingsUpdateRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail validation with invalid userActivated', () => {
|
||||
const data = {
|
||||
userActivated: 'invalid',
|
||||
allowSSOManualLogin: false,
|
||||
};
|
||||
|
||||
const result = SettingsUpdateRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe('userActivated');
|
||||
expect(result.error?.issues[0].message).toBe('Expected boolean, received string');
|
||||
});
|
||||
|
||||
it('should fail validation with invalid allowSSOManualLogin', () => {
|
||||
const data = {
|
||||
userActivated: true,
|
||||
allowSSOManualLogin: 'invalid',
|
||||
};
|
||||
|
||||
const result = SettingsUpdateRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path[0]).toBe('allowSSOManualLogin');
|
||||
expect(result.error?.issues[0].message).toBe('Expected boolean, received string');
|
||||
});
|
||||
|
||||
it('should pass validation with valid data', () => {
|
||||
const data = {
|
||||
userActivated: true,
|
||||
allowSSOManualLogin: false,
|
||||
};
|
||||
|
||||
const result = SettingsUpdateRequestDto.safeParse(data);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
import { UserUpdateRequestDto } from '../user-update-request.dto';
|
||||
|
||||
describe('UserUpdateRequestDto', () => {
|
||||
it('should fail validation for an invalid email', () => {
|
||||
const invalidRequest = {
|
||||
email: 'invalid-email',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
mfaCode: '123456',
|
||||
};
|
||||
|
||||
const result = UserUpdateRequestDto.safeParse(invalidRequest);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path).toEqual(['email']);
|
||||
});
|
||||
|
||||
it('should fail validation for a firstName with potential XSS attack', () => {
|
||||
const invalidRequest = {
|
||||
email: 'test@example.com',
|
||||
firstName: '<script>alert("XSS")</script>',
|
||||
lastName: 'Doe',
|
||||
mfaCode: '123456',
|
||||
};
|
||||
|
||||
const result = UserUpdateRequestDto.safeParse(invalidRequest);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path).toEqual(['firstName']);
|
||||
});
|
||||
|
||||
it('should fail validation for a firstName with a URL', () => {
|
||||
const invalidRequest = {
|
||||
email: 'test@example.com',
|
||||
firstName: 'test http://malicious.com',
|
||||
lastName: 'Doe',
|
||||
mfaCode: '123456',
|
||||
};
|
||||
|
||||
const result = UserUpdateRequestDto.safeParse(invalidRequest);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path).toEqual(['firstName']);
|
||||
});
|
||||
|
||||
it('should fail validation for a lastName with potential XSS attack', () => {
|
||||
const invalidRequest = {
|
||||
email: 'test@example.com',
|
||||
firstName: 'John',
|
||||
lastName: '<script>alert("XSS")</script>',
|
||||
mfaCode: '123456',
|
||||
};
|
||||
|
||||
const result = UserUpdateRequestDto.safeParse(invalidRequest);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path).toEqual(['lastName']);
|
||||
});
|
||||
|
||||
it('should fail validation for a lastName with a URL', () => {
|
||||
const invalidRequest = {
|
||||
email: 'test@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'testing http://malicious.com',
|
||||
mfaCode: '123456',
|
||||
};
|
||||
|
||||
const result = UserUpdateRequestDto.safeParse(invalidRequest);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.issues[0].path).toEqual(['lastName']);
|
||||
});
|
||||
|
||||
it('should validate a valid user update request', () => {
|
||||
const validRequest = {
|
||||
email: 'test@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
mfaCode: '123456',
|
||||
};
|
||||
|
||||
const result = UserUpdateRequestDto.safeParse(validRequest);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
export class PasswordUpdateRequestDto extends Z.class({
|
||||
currentPassword: z.string(),
|
||||
newPassword: z.string(),
|
||||
mfaCode: z.string().optional(),
|
||||
}) {}
|
|
@ -0,0 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
export class RoleChangeRequestDto extends Z.class({
|
||||
newRoleName: z.enum(['global:admin', 'global:member'], {
|
||||
required_error: 'New role is required',
|
||||
}),
|
||||
}) {}
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
export class SettingsUpdateRequestDto extends Z.class({
|
||||
userActivated: z.boolean().optional(),
|
||||
allowSSOManualLogin: z.boolean().optional(),
|
||||
}) {}
|
|
@ -0,0 +1,31 @@
|
|||
import xss from 'xss';
|
||||
import { z } from 'zod';
|
||||
import { Z } from 'zod-class';
|
||||
|
||||
const xssCheck = (value: string) =>
|
||||
value ===
|
||||
xss(value, {
|
||||
whiteList: {}, // no tags are allowed
|
||||
});
|
||||
|
||||
const URL_REGEX = /^(https?:\/\/|www\.)|(\.[\p{L}\d-]+)/iu;
|
||||
const urlCheck = (value: string) => !URL_REGEX.test(value);
|
||||
|
||||
const nameSchema = () =>
|
||||
z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(32)
|
||||
.refine(xssCheck, {
|
||||
message: 'Potentially malicious string',
|
||||
})
|
||||
.refine(urlCheck, {
|
||||
message: 'Potentially malicious string',
|
||||
});
|
||||
|
||||
export class UserUpdateRequestDto extends Z.class({
|
||||
email: z.string().email(),
|
||||
firstName: nameSchema().optional(),
|
||||
lastName: nameSchema().optional(),
|
||||
mfaCode: z.string().optional(),
|
||||
}) {}
|
171
packages/@n8n/api-types/src/frontend-settings.ts
Normal file
171
packages/@n8n/api-types/src/frontend-settings.ts
Normal file
|
@ -0,0 +1,171 @@
|
|||
import type { ExpressionEvaluatorType, LogLevel, WorkflowSettings } from 'n8n-workflow';
|
||||
|
||||
export interface IVersionNotificationSettings {
|
||||
enabled: boolean;
|
||||
endpoint: string;
|
||||
infoUrl: string;
|
||||
}
|
||||
|
||||
export interface ITelemetryClientConfig {
|
||||
url: string;
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface ITelemetrySettings {
|
||||
enabled: boolean;
|
||||
config?: ITelemetryClientConfig;
|
||||
}
|
||||
|
||||
export type AuthenticationMethod = 'email' | 'ldap' | 'saml';
|
||||
|
||||
export interface IUserManagementSettings {
|
||||
quota: number;
|
||||
showSetupOnFirstLoad?: boolean;
|
||||
smtpSetup: boolean;
|
||||
authenticationMethod: AuthenticationMethod;
|
||||
}
|
||||
|
||||
export interface FrontendSettings {
|
||||
isDocker?: boolean;
|
||||
databaseType: 'sqlite' | 'mariadb' | 'mysqldb' | 'postgresdb';
|
||||
endpointForm: string;
|
||||
endpointFormTest: string;
|
||||
endpointFormWaiting: string;
|
||||
endpointWebhook: string;
|
||||
endpointWebhookTest: string;
|
||||
saveDataErrorExecution: WorkflowSettings.SaveDataExecution;
|
||||
saveDataSuccessExecution: WorkflowSettings.SaveDataExecution;
|
||||
saveManualExecutions: boolean;
|
||||
saveExecutionProgress: boolean;
|
||||
executionTimeout: number;
|
||||
maxExecutionTimeout: number;
|
||||
workflowCallerPolicyDefaultOption: WorkflowSettings.CallerPolicy;
|
||||
oauthCallbackUrls: {
|
||||
oauth1: string;
|
||||
oauth2: string;
|
||||
};
|
||||
timezone: string;
|
||||
urlBaseWebhook: string;
|
||||
urlBaseEditor: string;
|
||||
versionCli: string;
|
||||
nodeJsVersion: string;
|
||||
concurrency: number;
|
||||
authCookie: {
|
||||
secure: boolean;
|
||||
};
|
||||
binaryDataMode: 'default' | 'filesystem' | 's3';
|
||||
releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev';
|
||||
n8nMetadata?: {
|
||||
userId?: string;
|
||||
[key: string]: string | number | undefined;
|
||||
};
|
||||
versionNotifications: IVersionNotificationSettings;
|
||||
instanceId: string;
|
||||
telemetry: ITelemetrySettings;
|
||||
posthog: {
|
||||
enabled: boolean;
|
||||
apiHost: string;
|
||||
apiKey: string;
|
||||
autocapture: boolean;
|
||||
disableSessionRecording: boolean;
|
||||
debug: boolean;
|
||||
};
|
||||
personalizationSurveyEnabled: boolean;
|
||||
defaultLocale: string;
|
||||
userManagement: IUserManagementSettings;
|
||||
sso: {
|
||||
saml: {
|
||||
loginLabel: string;
|
||||
loginEnabled: boolean;
|
||||
};
|
||||
ldap: {
|
||||
loginLabel: string;
|
||||
loginEnabled: boolean;
|
||||
};
|
||||
};
|
||||
publicApi: {
|
||||
enabled: boolean;
|
||||
latestVersion: number;
|
||||
path: string;
|
||||
swaggerUi: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
workflowTagsDisabled: boolean;
|
||||
logLevel: LogLevel;
|
||||
hiringBannerEnabled: boolean;
|
||||
previewMode: boolean;
|
||||
templates: {
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
};
|
||||
missingPackages?: boolean;
|
||||
executionMode: 'regular' | 'queue';
|
||||
pushBackend: 'sse' | 'websocket';
|
||||
communityNodesEnabled: boolean;
|
||||
aiAssistant: {
|
||||
enabled: boolean;
|
||||
};
|
||||
deployment: {
|
||||
type: string;
|
||||
};
|
||||
allowedModules: {
|
||||
builtIn?: string[];
|
||||
external?: string[];
|
||||
};
|
||||
enterprise: {
|
||||
sharing: boolean;
|
||||
ldap: boolean;
|
||||
saml: boolean;
|
||||
logStreaming: boolean;
|
||||
advancedExecutionFilters: boolean;
|
||||
variables: boolean;
|
||||
sourceControl: boolean;
|
||||
auditLogs: boolean;
|
||||
externalSecrets: boolean;
|
||||
showNonProdBanner: boolean;
|
||||
debugInEditor: boolean;
|
||||
binaryDataS3: boolean;
|
||||
workflowHistory: boolean;
|
||||
workerView: boolean;
|
||||
advancedPermissions: boolean;
|
||||
projects: {
|
||||
team: {
|
||||
limit: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
hideUsagePage: boolean;
|
||||
license: {
|
||||
planName?: string;
|
||||
consumerId: string;
|
||||
environment: 'development' | 'production' | 'staging';
|
||||
};
|
||||
variables: {
|
||||
limit: number;
|
||||
};
|
||||
expressions: {
|
||||
evaluator: ExpressionEvaluatorType;
|
||||
};
|
||||
mfa: {
|
||||
enabled: boolean;
|
||||
};
|
||||
banners: {
|
||||
dismissed: string[];
|
||||
};
|
||||
ai: {
|
||||
enabled: boolean;
|
||||
};
|
||||
workflowHistory: {
|
||||
pruneTime: number;
|
||||
licensePruneTime: number;
|
||||
};
|
||||
pruning: {
|
||||
isEnabled: boolean;
|
||||
maxAge: number;
|
||||
maxCount: number;
|
||||
};
|
||||
security: {
|
||||
blockFileAccessToN8nFiles: boolean;
|
||||
};
|
||||
}
|
9
packages/@n8n/api-types/src/index.ts
Normal file
9
packages/@n8n/api-types/src/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export type * from './datetime';
|
||||
export * from './dto';
|
||||
export type * from './push';
|
||||
export type * from './scaling';
|
||||
export type * from './frontend-settings';
|
||||
export type * from './user';
|
||||
|
||||
export type { Collaborator } from './push/collaboration';
|
||||
export type { SendWorkerStatusMessage } from './push/worker';
|
17
packages/@n8n/api-types/src/push/collaboration.ts
Normal file
17
packages/@n8n/api-types/src/push/collaboration.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { Iso8601DateTimeString } from '../datetime';
|
||||
import type { MinimalUser } from '../user';
|
||||
|
||||
export type Collaborator = {
|
||||
user: MinimalUser;
|
||||
lastSeen: Iso8601DateTimeString;
|
||||
};
|
||||
|
||||
type CollaboratorsChanged = {
|
||||
type: 'collaboratorsChanged';
|
||||
data: {
|
||||
workflowId: string;
|
||||
collaborators: Collaborator[];
|
||||
};
|
||||
};
|
||||
|
||||
export type CollaborationPushMessage = CollaboratorsChanged;
|
9
packages/@n8n/api-types/src/push/debug.ts
Normal file
9
packages/@n8n/api-types/src/push/debug.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
type SendConsoleMessage = {
|
||||
type: 'sendConsoleMessage';
|
||||
data: {
|
||||
source: string;
|
||||
messages: unknown[];
|
||||
};
|
||||
};
|
||||
|
||||
export type DebugPushMessage = SendConsoleMessage;
|
53
packages/@n8n/api-types/src/push/execution.ts
Normal file
53
packages/@n8n/api-types/src/push/execution.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import type { IRun, ITaskData, WorkflowExecuteMode } from 'n8n-workflow';
|
||||
|
||||
type ExecutionStarted = {
|
||||
type: 'executionStarted';
|
||||
data: {
|
||||
executionId: string;
|
||||
mode: WorkflowExecuteMode;
|
||||
startedAt: Date;
|
||||
workflowId: string;
|
||||
workflowName?: string;
|
||||
retryOf?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ExecutionFinished = {
|
||||
type: 'executionFinished';
|
||||
data: {
|
||||
executionId: string;
|
||||
data: IRun;
|
||||
retryOf?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type ExecutionRecovered = {
|
||||
type: 'executionRecovered';
|
||||
data: {
|
||||
executionId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type NodeExecuteBefore = {
|
||||
type: 'nodeExecuteBefore';
|
||||
data: {
|
||||
executionId: string;
|
||||
nodeName: string;
|
||||
};
|
||||
};
|
||||
|
||||
type NodeExecuteAfter = {
|
||||
type: 'nodeExecuteAfter';
|
||||
data: {
|
||||
executionId: string;
|
||||
nodeName: string;
|
||||
data: ITaskData;
|
||||
};
|
||||
};
|
||||
|
||||
export type ExecutionPushMessage =
|
||||
| ExecutionStarted
|
||||
| ExecutionFinished
|
||||
| ExecutionRecovered
|
||||
| NodeExecuteBefore
|
||||
| NodeExecuteAfter;
|
21
packages/@n8n/api-types/src/push/hot-reload.ts
Normal file
21
packages/@n8n/api-types/src/push/hot-reload.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
type NodeTypeData = {
|
||||
name: string;
|
||||
version: number;
|
||||
};
|
||||
|
||||
type ReloadNodeType = {
|
||||
type: 'reloadNodeType';
|
||||
data: NodeTypeData;
|
||||
};
|
||||
|
||||
type RemoveNodeType = {
|
||||
type: 'removeNodeType';
|
||||
data: NodeTypeData;
|
||||
};
|
||||
|
||||
type NodeDescriptionUpdated = {
|
||||
type: 'nodeDescriptionUpdated';
|
||||
data: {};
|
||||
};
|
||||
|
||||
export type HotReloadPushMessage = ReloadNodeType | RemoveNodeType | NodeDescriptionUpdated;
|
20
packages/@n8n/api-types/src/push/index.ts
Normal file
20
packages/@n8n/api-types/src/push/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import type { CollaborationPushMessage } from './collaboration';
|
||||
import type { DebugPushMessage } from './debug';
|
||||
import type { ExecutionPushMessage } from './execution';
|
||||
import type { HotReloadPushMessage } from './hot-reload';
|
||||
import type { WebhookPushMessage } from './webhook';
|
||||
import type { WorkerPushMessage } from './worker';
|
||||
import type { WorkflowPushMessage } from './workflow';
|
||||
|
||||
export type PushMessage =
|
||||
| ExecutionPushMessage
|
||||
| WorkflowPushMessage
|
||||
| HotReloadPushMessage
|
||||
| WebhookPushMessage
|
||||
| WorkerPushMessage
|
||||
| CollaborationPushMessage
|
||||
| DebugPushMessage;
|
||||
|
||||
export type PushType = PushMessage['type'];
|
||||
|
||||
export type PushPayload<T extends PushType> = Extract<PushMessage, { type: T }>['data'];
|
17
packages/@n8n/api-types/src/push/webhook.ts
Normal file
17
packages/@n8n/api-types/src/push/webhook.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
type TestWebhookDeleted = {
|
||||
type: 'testWebhookDeleted';
|
||||
data: {
|
||||
executionId?: string;
|
||||
workflowId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type TestWebhookReceived = {
|
||||
type: 'testWebhookReceived';
|
||||
data: {
|
||||
executionId: string;
|
||||
workflowId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type WebhookPushMessage = TestWebhookDeleted | TestWebhookReceived;
|
11
packages/@n8n/api-types/src/push/worker.ts
Normal file
11
packages/@n8n/api-types/src/push/worker.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import type { WorkerStatus } from '../scaling';
|
||||
|
||||
export type SendWorkerStatusMessage = {
|
||||
type: 'sendWorkerStatusMessage';
|
||||
data: {
|
||||
workerId: string;
|
||||
status: WorkerStatus;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkerPushMessage = SendWorkerStatusMessage;
|
26
packages/@n8n/api-types/src/push/workflow.ts
Normal file
26
packages/@n8n/api-types/src/push/workflow.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
type WorkflowActivated = {
|
||||
type: 'workflowActivated';
|
||||
data: {
|
||||
workflowId: string;
|
||||
};
|
||||
};
|
||||
|
||||
type WorkflowFailedToActivate = {
|
||||
type: 'workflowFailedToActivate';
|
||||
data: {
|
||||
workflowId: string;
|
||||
errorMessage: string;
|
||||
};
|
||||
};
|
||||
|
||||
type WorkflowDeactivated = {
|
||||
type: 'workflowDeactivated';
|
||||
data: {
|
||||
workflowId: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type WorkflowPushMessage =
|
||||
| WorkflowActivated
|
||||
| WorkflowFailedToActivate
|
||||
| WorkflowDeactivated;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue