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

This commit is contained in:
Jonathan Bennetts 2024-01-16 09:58:07 +00:00
commit aab002c74f
2723 changed files with 128441 additions and 37387 deletions

View file

@ -7,3 +7,7 @@
# refactor(editor): Apply Prettier (no-changelog) #4920 # refactor(editor): Apply Prettier (no-changelog) #4920
5ca2148c7ed06c90f999508928b7a51f9ac7a788 5ca2148c7ed06c90f999508928b7a51f9ac7a788
# refactor: Run lintfix (no-changelog) (#7537)
62c096710fab2f7e886518abdbded34b55e93f62

View file

@ -1 +1,16 @@
Github issue / Community forum post (link here to close automatically): ## Summary
> Describe what the PR does and how to test. Photos and videos are recommended.
## Related tickets and issues
> Include links to **Linear ticket** or Github issue or Community forum post. Important in order to close *automatically* and provide context to reviewers.
## Review / Merge checklist
- [ ] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md))
- [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created.
- [ ] Tests included.
> A bug is not considered fixed, unless a test is added to prevent it from happening again.
> A feature is not complete without tests.

View file

@ -0,0 +1,112 @@
# PR Title Convention
We have very precise rules over how Pull Requests (to the `master` branch) must be formatted. This format basically follows the [Angular Commit Message Convention](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit). It leads to easier to read commit history and allows for automated generation of release notes:
A PR title consists of these elements:
```
<type>(<scope>): <summary>
│ │ │
│ │ └─⫸ Summary: In imperative present tense.
| | Capitalized
| | No period at the end.
│ │
│ └─⫸ Scope: API|core|editor|* Node
└─⫸ Type: build|ci|docs|feat|fix|perf|refactor|test
```
- PR title
- type
- scope (*optional*)
- summary
- PR description
- body (optional)
- blank line
- footer (optional)
The structure looks like this:
### **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: gulp, broccoli, npm)
- `ci` - Changes to our CI configuration files and scripts (e.g. Github actions)
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.
### **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
- `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
### **Summary**
The summary contains succinct description of the change:
- use the imperative, present tense: "change" not "changed" nor "changes"
- capitalize the first letter
- *no* dot (.) at the end
- 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)**
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)**
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:
```
BREAKING CHANGE: <breaking change summary>
<BLANK LINE>
<breaking change description + migration instructions>
<BLANK LINE>
<BLANK LINE>
Fixes #<issue number>
```
or
```
DEPRECATED: <what is deprecated>
<BLANK LINE>
<deprecation description + recommended update path>
<BLANK LINE>
<BLANK LINE>
Closes #<pr number>
```
A Breaking Change section should start with the phrase "`BREAKING CHANGE:` " followed by a summary of the breaking change, a blank line, and a detailed description of the breaking change that also includes migration instructions.
> 💡 A breaking change can additionally also be marked by adding a “`!`” to the header, right before the “`:`”, e.g. `feat(editor)!: Remove support for dark mode`
>
> This makes locating breaking changes easier when just skimming through commit messages.
> 💡 The breaking changes must also be added to the [packages/cli/BREAKING-CHANGES.md](https://github.com/n8n-io/n8n/blob/master/packages/cli/BREAKING-CHANGES.md) file located in the n8n repository.
Similarly, a Deprecation section should start with "`DEPRECATED:` " followed by a short description of what is deprecated, a blank line, and a detailed description of the deprecation that also mentions the recommended update path.
### **Revert commits**
If the commit reverts a previous commit, it should begin with `revert:` , followed by the header of the reverted commit.
The content of the commit message body should contain:
- information about the SHA of the commit being reverted in the following format: `This reverts commit <SHA>`,
- a clear description of the reason for reverting the commit message.

View file

@ -43,7 +43,7 @@ for (const packageName in packageMap) {
packageJson.version = packageMap[packageName].nextVersion = packageJson.version = packageMap[packageName].nextVersion =
isDirty || isDirty ||
Object.keys(packageJson.dependencies).some( Object.keys(packageJson.dependencies || {}).some(
(dependencyName) => packageMap[dependencyName]?.isDirty, (dependencyName) => packageMap[dependencyName]?.isDirty,
) )
? semver.inc(version, releaseType) ? semver.inc(version, releaseType)

View file

@ -1,9 +1,12 @@
{ {
"dependencies": { "dependencies": {
"cacheable-lookup": "6.1.0",
"conventional-changelog": "^4.0.0", "conventional-changelog": "^4.0.0",
"glob": "^10.3.0", "debug": "4.3.4",
"semver": "^7.5.4", "glob": "10.3.10",
"tempfile": "^5.0.0", "p-limit": "3.1.0",
"semver": "7.5.4",
"tempfile": "5.0.0",
"typescript": "*" "typescript": "*"
} }
} }

View file

@ -1,11 +1,19 @@
#!/usr/bin/env node #!/usr/bin/env node
const packages = ['nodes-base', '@n8n/nodes-langchain'];
const concurrency = 20;
let exitCode = 0;
const debug = require('debug')('n8n');
const path = require('path'); const path = require('path');
const https = require('https'); const https = require('https');
const glob = require('fast-glob'); const glob = require('glob');
const pLimit = require('p-limit'); const pLimit = require('p-limit');
const Lookup = require('cacheable-lookup').default;
const nodesBaseDir = path.resolve(__dirname, '../packages/nodes-base'); const agent = new https.Agent({ keepAlive: true, keepAliveMsecs: 5000 });
new Lookup().install(agent);
const limiter = pLimit(concurrency);
const validateUrl = async (kind, name, documentationUrl) => const validateUrl = async (kind, name, documentationUrl) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
@ -22,21 +30,26 @@ const validateUrl = async (kind, name, documentationUrl) =>
port: 443, port: 443,
path: url.pathname, path: url.pathname,
method: 'HEAD', method: 'HEAD',
agent,
},
(res) => {
debug('✓', kind, name);
resolve([name, res.statusCode]);
}, },
(res) => resolve([name, res.statusCode]),
) )
.on('error', (e) => reject(e)) .on('error', (e) => reject(e))
.end(); .end();
}); });
const checkLinks = async (kind) => { const checkLinks = async (baseDir, kind) => {
let types = require(path.join(nodesBaseDir, `dist/types/${kind}.json`)); let types = require(path.join(baseDir, `dist/types/${kind}.json`));
if (kind === 'nodes') if (kind === 'nodes')
types = types.filter(({ codex }) => !!codex?.resources?.primaryDocumentation); types = types.filter(({ codex }) => !!codex?.resources?.primaryDocumentation);
const limit = pLimit(30); debug(kind, types.length);
const statuses = await Promise.all( const statuses = await Promise.all(
types.map((type) => types.map((type) =>
limit(() => { limiter(() => {
const documentationUrl = const documentationUrl =
kind === 'credentials' kind === 'credentials'
? type.documentationUrl ? type.documentationUrl
@ -55,10 +68,13 @@ const checkLinks = async (kind) => {
if (missingDocs.length) console.log('Documentation URL missing for %s', kind, missingDocs); if (missingDocs.length) console.log('Documentation URL missing for %s', kind, missingDocs);
if (invalidUrls.length) console.log('Documentation URL invalid for %s', kind, invalidUrls); if (invalidUrls.length) console.log('Documentation URL invalid for %s', kind, invalidUrls);
if (missingDocs.length || invalidUrls.length) process.exit(1); if (missingDocs.length || invalidUrls.length) exitCode = 1;
}; };
(async () => { (async () => {
await checkLinks('credentials'); for (const packageName of packages) {
await checkLinks('nodes'); const baseDir = path.resolve(__dirname, '../../packages', packageName);
await Promise.all([checkLinks(baseDir, 'credentials'), checkLinks(baseDir, 'nodes')]);
if (exitCode !== 0) process.exit(exitCode);
}
})(); })();

View file

@ -27,16 +27,18 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Build nodes-base - name: Build nodes-base
run: pnpm --filter @n8n/client-oauth2 --filter n8n-workflow --filter n8n-core --filter n8n-nodes-base build run: pnpm --filter @n8n/client-oauth2 --filter n8n-workflow --filter n8n-core --filter n8n-nodes-base --filter @n8n/n8n-nodes-langchain build
- name: Test URLS - run: npm install --prefix=.github/scripts --no-package-lock
run: node scripts/validate-docs-links.js
- name: Test URLs
run: node .github/scripts/validate-docs-links.js
- name: Notify Slack on failure - name: Notify Slack on failure
uses: act10ns/slack@v2.0.0 uses: act10ns/slack@v2.0.0
if: failure() if: failure()
with: with:
status: ${{ job.status }} status: ${{ job.status }}
channel: '#updates-build-alerts' channel: '#mission-docs'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: Documentation URLs check failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) message: Documentation URLs check failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})

View file

@ -30,6 +30,6 @@ jobs:
- name: Validate PR title - name: Validate PR title
id: validate_pr_title id: validate_pr_title
uses: n8n-io/validate-n8n-pull-request-title@v1.1 uses: n8n-io/validate-n8n-pull-request-title@v1.3
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,22 +0,0 @@
name: PR Checklist
on:
pull_request_target:
types:
- opened
- synchronize
branches:
- master
jobs:
checklist_job:
runs-on: ubuntu-latest
name: Checklist job
steps:
- name: Checkout
uses: actions/checkout@v3.5.3
- name: Checklist
uses: wyozi/contextual-qa-checklist-action@master
with:
gh-token: ${{ secrets.GITHUB_TOKEN }}
comment-footer: Make sure to check off this list before asking for review.

View file

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [18.x, 20.5] node-version: [18.x, 20.x]
steps: steps:
- uses: actions/checkout@v3.5.3 - uses: actions/checkout@v3.5.3
@ -44,11 +44,12 @@ jobs:
needs: install-and-build needs: install-and-build
strategy: strategy:
matrix: matrix:
node-version: [18.x, 20.5] node-version: [18.x, 20.x]
with: with:
ref: ${{ inputs.branch }} ref: ${{ inputs.branch }}
nodeVersion: ${{ matrix.node-version }} nodeVersion: ${{ matrix.node-version }}
cacheKey: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint cacheKey: ${{ github.sha }}-base:${{ matrix.node-version }}-test-lint
collectCoverage: true
lint: lint:
name: Lint changes name: Lint changes
@ -56,7 +57,7 @@ jobs:
needs: install-and-build needs: install-and-build
strategy: strategy:
matrix: matrix:
node-version: [18.x, 20.5] node-version: [18.x, 20.x]
steps: steps:
- uses: actions/checkout@v3.5.3 - uses: actions/checkout@v3.5.3
with: with:
@ -95,6 +96,6 @@ jobs:
if: failure() if: failure()
with: with:
status: ${{ job.status }} status: ${{ job.status }}
channel: '#updates-build-alerts' channel: '#alerts-build'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: Master branch (build or test or lint) failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) message: Master branch (build or test or lint) failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})

View file

@ -6,7 +6,11 @@ on:
workflow_dispatch: workflow_dispatch:
pull_request: pull_request:
paths: paths:
- packages/cli/src/databases/migrations/** - packages/cli/src/databases/**
concurrency:
group: db-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
build: build:
@ -22,7 +26,7 @@ jobs:
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Build Backend - name: Build Backend
run: pnpm --filter @n8n/client-oauth2 --filter n8n-workflow --filter n8n-core --filter n8n build run: pnpm build:backend
- name: Cache build artifacts - name: Cache build artifacts
uses: actions/cache/save@v3.3.1 uses: actions/cache/save@v3.3.1
@ -61,7 +65,7 @@ jobs:
- name: Test MySQL - name: Test MySQL
working-directory: packages/cli working-directory: packages/cli
run: DB_TABLE_PREFIX=test_ pnpm test:mysql --runInBand run: pnpm test:mysql
postgres: postgres:
name: Postgres name: Postgres
@ -94,7 +98,7 @@ jobs:
- name: Test Postgres - name: Test Postgres
working-directory: packages/cli working-directory: packages/cli
run: DB_POSTGRESDB_SCHEMA=alt_schema DB_TABLE_PREFIX=test_ pnpm test:postgres --runInBand run: pnpm test:postgres
notify-on-failure: notify-on-failure:
name: Notify Slack on failure name: Notify Slack on failure
@ -106,6 +110,6 @@ jobs:
if: failure() && github.ref == 'refs/heads/master' if: failure() && github.ref == 'refs/heads/master'
with: with:
status: ${{ job.status }} status: ${{ job.status }}
channel: '#updates-build-alerts' channel: '#alerts-build'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: Postgres or MySQL tests failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) message: Postgres or MySQL tests failed (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})

View file

@ -35,6 +35,11 @@ on:
description: 'URL to call after Docker Image got built successfully.' description: 'URL to call after Docker Image got built successfully.'
required: false required: false
default: '' default: ''
include-arm64:
description: 'Include ARM64 support'
type: boolean
required: true
default: false
jobs: jobs:
build: build:
@ -76,7 +81,7 @@ jobs:
build-args: | build-args: |
N8N_RELEASE_TYPE=nightly N8N_RELEASE_TYPE=nightly
file: ./docker/images/n8n-custom/Dockerfile file: ./docker/images/n8n-custom/Dockerfile
platforms: linux/amd64 platforms: ${{ github.event.inputs.include-arm64 == 'true' && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
provenance: false provenance: false
push: true push: true
tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.tag || 'nightly' }} tags: ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.tag || 'nightly' }}

View file

@ -40,7 +40,7 @@ on:
containers: containers:
description: 'Number of containers to run tests in.' description: 'Number of containers to run tests in.'
required: false required: false
default: '[1, 2, 3, 4, 5, 6, 7, 8]' default: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]'
type: string type: string
pr_number: pr_number:
description: 'PR number to run tests for.' description: 'PR number to run tests for.'
@ -99,6 +99,8 @@ jobs:
runTests: false runTests: false
install: false install: false
build: pnpm build build: pnpm build
env:
VUE_APP_MAX_PINNED_DATA_SIZE: 16384
- name: Cypress install - name: Cypress install
run: pnpm cypress:install run: pnpm cypress:install

View file

@ -6,6 +6,10 @@ on:
branch: branch:
- 'master' - 'master'
concurrency:
group: e2e-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs: jobs:
run-e2e-tests: run-e2e-tests:
name: E2E [Electron/Node 18] name: E2E [Electron/Node 18]

View file

@ -60,7 +60,7 @@ jobs:
if: failure() if: failure()
with: with:
status: ${{ job.status }} status: ${{ job.status }}
channel: '#updates-build-alerts' channel: '#alerts-build'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: E2E failure for branch `${{ inputs.branch || 'master' }}` deployed by ${{ inputs.user || 'schedule' }} (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) message: E2E failure for branch `${{ inputs.branch || 'master' }}` deployed by ${{ inputs.user || 'schedule' }} (${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})

View file

@ -36,6 +36,9 @@ jobs:
- name: Build - name: Build
run: pnpm build run: pnpm build
- name: Dry-run publishing
run: pnpm publish -r --no-git-checks --dry-run
- name: Publish to NPM - name: Publish to NPM
run: | run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc

View file

@ -39,4 +39,17 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- run: docker buildx imagetools create -t n8nio/n8n:${{ github.event.inputs.release-channel }} n8nio/n8n:${{ github.event.inputs.version }} - run: docker buildx imagetools create -t ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.release-channel }} ${{ secrets.DOCKER_USERNAME }}/n8n:${{ github.event.inputs.version }}
release-to-github-container-registry:
name: Release to GitHub Container Registry
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: docker/login-action@v2.2.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- run: docker buildx imagetools create -t ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.release-channel }} ghcr.io/${{ github.repository_owner }}/n8n:${{ github.event.inputs.version }}

View file

@ -45,7 +45,7 @@ jobs:
working-directory: n8n working-directory: n8n
run: | run: |
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
pnpm --filter @n8n/client-oauth2 --filter n8n-workflow --filter n8n-core --filter n8n-nodes-base --filter n8n build pnpm build:backend
shell: bash shell: bash
- name: Import credentials - name: Import credentials
@ -96,7 +96,7 @@ jobs:
if: failure() if: failure()
with: with:
status: ${{ job.status }} status: ${{ job.status }}
channel: '#updates-build-alerts' channel: '#alerts-build'
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
message: | message: |
🛑 Workflow test failed 🛑: 🛑 Workflow test failed 🛑:

View file

@ -18,11 +18,17 @@ on:
required: false required: false
default: '' default: ''
type: string type: string
collectCoverage:
required: false
default: 'false'
type: string
jobs: jobs:
unit-test: unit-test:
name: Unit tests name: Unit tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
COVERAGE_ENABLED: ${{ inputs.collectCoverage }}
steps: steps:
- uses: actions/checkout@v3.5.3 - uses: actions/checkout@v3.5.3
with: with:
@ -51,10 +57,17 @@ jobs:
path: ./packages/**/dist path: ./packages/**/dist
key: ${{ inputs.cacheKey }} key: ${{ inputs.cacheKey }}
- name: Test - name: Test Backend
run: pnpm test run: pnpm test:backend
- name: Test Nodes
run: pnpm test:nodes
- name: Test Frontend
run: pnpm test:frontend
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: ${{ inputs.collectCoverage == 'true' }}
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
with: with:
files: packages/@n8n/client-oauth2/coverage/cobertura-coverage.xml,packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml files: packages/@n8n/chat/coverage/cobertura-coverage.xml,packages/@n8n/nodes-langchain/coverage/cobertura-coverage.xml,packages/@n8n/permissions/coverage/cobertura-coverage.xml,packages/@n8n/client-oauth2/coverage/cobertura-coverage.xml,packages/cli/coverage/cobertura-coverage.xml,packages/core/coverage/cobertura-coverage.xml,packages/design-system/coverage/cobertura-coverage.xml,packages/editor-ui/coverage/cobertura-coverage.xml,packages/nodes-base/coverage/cobertura-coverage.xml,packages/workflow/coverage/cobertura-coverage.xml

View file

@ -1,3 +1,444 @@
# [1.24.0](https://github.com/n8n-io/n8n/compare/n8n@1.23.0...n8n@1.24.0) (2024-01-10)
### Bug Fixes
* **core:** Do not add Authentication header when `authentication` type is `body` ([#8201](https://github.com/n8n-io/n8n/issues/8201)) ([ac1c642](https://github.com/n8n-io/n8n/commit/ac1c642fddfac3b0ed1144c7eccd7c88fbd5a1a5))
* **core:** Fix test webhook deregistration ([#8247](https://github.com/n8n-io/n8n/issues/8247)) ([5032bf0](https://github.com/n8n-io/n8n/commit/5032bf0e346dccf7cade17a1518b3031118af5e1))
* **editor:** Items count display in running workflow ([#8148](https://github.com/n8n-io/n8n/issues/8148)) ([8a3c87f](https://github.com/n8n-io/n8n/commit/8a3c87f69c20de7c713dff021e390ea4ea32b103)), closes [/github.com/n8n-io/n8n/pull/7763/files#diff-f5dae80a64b9951bb6691f1b9d439423cf84fa0cc9601b3f2c00904f3135e8deR48](https://github.com//github.com/n8n-io/n8n/pull/7763/files/issues/diff-f5dae80a64b9951bb6691f1b9d439423cf84fa0cc9601b3f2c00904f3135e8deR48)
* **editor:** Only load suggested templates for owners ([#8228](https://github.com/n8n-io/n8n/issues/8228)) ([8f22a26](https://github.com/n8n-io/n8n/commit/8f22a265d607047eff22ba957d627bbec7da7900))
* **editor:** Tweaking button sizes in execution preview ([#8206](https://github.com/n8n-io/n8n/issues/8206)) ([9d40ae8](https://github.com/n8n-io/n8n/commit/9d40ae8b74594d4368591a62f9b39dde28efc64d))
* **editor:** Unify canvas button positioning ([#8258](https://github.com/n8n-io/n8n/issues/8258)) ([b6c42cc](https://github.com/n8n-io/n8n/commit/b6c42cc08408d9d7cc49cc84245b4ad515fa3e6a))
* **editor:** Vertically center workflow preview loading state ([#8231](https://github.com/n8n-io/n8n/issues/8231)) ([2d6e406](https://github.com/n8n-io/n8n/commit/2d6e406e215188dbbbeb593ac09ccad3914aaf81))
* Fix issue with API key being required for the Qdrant Node ([#8237](https://github.com/n8n-io/n8n/issues/8237)) ([4401db3](https://github.com/n8n-io/n8n/commit/4401db3a2fad3464a5498e9a86fc6bba4f9c9f95))
* Fix template credential setup for nodes that dont have credentials ([#8208](https://github.com/n8n-io/n8n/issues/8208)) ([cd3f5b5](https://github.com/n8n-io/n8n/commit/cd3f5b5b1f48e42cb6fa5ebcc15527c28502ceb9))
* Fix user reinvites on FE and BE ([#8261](https://github.com/n8n-io/n8n/issues/8261)) ([0dabe5c](https://github.com/n8n-io/n8n/commit/0dabe5c74e5ad0969d4691b3db4a1e796ed8a08c))
* **FTP Node:** FTP connection failed due to missing password credential in node ([#8131](https://github.com/n8n-io/n8n/issues/8131)) ([e056aa9](https://github.com/n8n-io/n8n/commit/e056aa9c4dd6c6a7717202029b25f4f65ddecb0d))
* **Github Trigger Node:** Enforce SSL validation by default ([#8265](https://github.com/n8n-io/n8n/issues/8265)) ([1387541](https://github.com/n8n-io/n8n/commit/1387541e336e7311ba9c43907fa95d3196fae2eb))
* Make params panel double width for all SQL nodes ([#8236](https://github.com/n8n-io/n8n/issues/8236)) ([048b588](https://github.com/n8n-io/n8n/commit/048b588852f5fed1c976889ba54ef564ca7f4894))
* **Monday.com Node:** Migrate to api 2023-10 ([#8254](https://github.com/n8n-io/n8n/issues/8254)) ([ccde38a](https://github.com/n8n-io/n8n/commit/ccde38a8a8d65a21bf4d38ef7b09a5ffa3c7ad2d))
* **MySQL Node:** Only escape table names when needed ([#8246](https://github.com/n8n-io/n8n/issues/8246)) ([3b01eb6](https://github.com/n8n-io/n8n/commit/3b01eb60c98d51d0d7572342b8d6d40763293719))
* **Nextcloud Node:** Throw an actual error if server responded with Fatal error ([#8234](https://github.com/n8n-io/n8n/issues/8234)) ([b201ff8](https://github.com/n8n-io/n8n/commit/b201ff8f23b2bac6b00d5c16d91b4b2931f45ade))
* **NocoDB Node:** Download attachments ([#8235](https://github.com/n8n-io/n8n/issues/8235)) ([43e8e5e](https://github.com/n8n-io/n8n/commit/43e8e5e540b9fcbca663fcf17a78a7aba2abb475))
* **Postgres Node:** Stop marking autogenerated columns as required ([#8230](https://github.com/n8n-io/n8n/issues/8230)) ([bed04ec](https://github.com/n8n-io/n8n/commit/bed04ec122234b4329a5e415bf3627c115b3f32e)), closes [#7084](https://github.com/n8n-io/n8n/issues/7084)
* Resolve expressions in credentials following paired item ([#8250](https://github.com/n8n-io/n8n/issues/8250)) ([ccb2b07](https://github.com/n8n-io/n8n/commit/ccb2b076f8240b0712949b72ec57ae72a36ef62d))
* **Set Node:** Field not excluded if dot notation disabled and field was set by using drag-and-drop from ui ([#8233](https://github.com/n8n-io/n8n/issues/8233)) ([cda49a4](https://github.com/n8n-io/n8n/commit/cda49a4747ef4369ce7a971872c6fb8a74f4156d))
* Store workflow settings when saving an execution ([#8288](https://github.com/n8n-io/n8n/issues/8288)) ([8a7c629](https://github.com/n8n-io/n8n/commit/8a7c629ea183f75f9916003edf11cb8aeef445eb))
* **Webhook Node:** Fix handling of form-data files ([#8256](https://github.com/n8n-io/n8n/issues/8256)) ([fc29030](https://github.com/n8n-io/n8n/commit/fc2903096e6e64e5b2a14593418d5651e07ca9ee))
### Features
* Add Chat Trigger node ([#7409](https://github.com/n8n-io/n8n/issues/7409)) ([af49e95](https://github.com/n8n-io/n8n/commit/af49e95cc7ccf70f233f9bd1e34fbb57f7f08ccf))
* **core:** Cache test webhook registrations ([#8176](https://github.com/n8n-io/n8n/issues/8176)) ([22a5f52](https://github.com/n8n-io/n8n/commit/22a5f5258da0a973e1ad44c0d3d4f0fda1d23444)), closes [#8155](https://github.com/n8n-io/n8n/issues/8155)
* **core:** Validate shutdown handlers on startup ([#8260](https://github.com/n8n-io/n8n/issues/8260)) ([3b996a7](https://github.com/n8n-io/n8n/commit/3b996a7da0137a75c3047656a4bc8cc336ebfc1e))
* **editor:** Add fullscreen view to code editor ([#8084](https://github.com/n8n-io/n8n/issues/8084)) ([071e6d6](https://github.com/n8n-io/n8n/commit/071e6d6b6e32b7196f34043710c23331ad28fac0))
* **editor:** Update copy: `Execute` --> `Test` ([#8137](https://github.com/n8n-io/n8n/issues/8137)) ([df5d07b](https://github.com/n8n-io/n8n/commit/df5d07bcb8beba760bc17118b36ccd531bc3c755))
* **Google Sheets Node:** Add "By Name" option to selector for Sheets ([#8241](https://github.com/n8n-io/n8n/issues/8241)) ([dce28f9](https://github.com/n8n-io/n8n/commit/dce28f9cb98db33bf22bcfee181f8e9ca64dd2bc))
* **HTTP Request Node:** Interval Between Requests option for pagination ([#8224](https://github.com/n8n-io/n8n/issues/8224)) ([270328c](https://github.com/n8n-io/n8n/commit/270328ccf6e5502adc092f6f85d146ffb98e1208))
* Implement MistralCloud Chat & Embeddings nodes ([#8239](https://github.com/n8n-io/n8n/issues/8239)) ([d37b908](https://github.com/n8n-io/n8n/commit/d37b9084b2c657d8b5b8bae6dbb51b428db26e1e))
* **MongoDB Node:** Add support for TLS ([#8266](https://github.com/n8n-io/n8n/issues/8266)) ([e796e7f](https://github.com/n8n-io/n8n/commit/e796e7f06d73a74a403000c53942d56cab91781b))
* **Switch Node:** Overhaul ([#7855](https://github.com/n8n-io/n8n/issues/7855)) ([f4092a9](https://github.com/n8n-io/n8n/commit/f4092a9e49f66845612420ba59a013796ed80d45))
### Performance Improvements
* **core:** Improve caching service ([#8213](https://github.com/n8n-io/n8n/issues/8213)) ([f53c482](https://github.com/n8n-io/n8n/commit/f53c482939db938c47523ac11a9538e35e1926a9)), closes [#7747](https://github.com/n8n-io/n8n/issues/7747)
* **core:** Optimize workflow activation errors ([#8242](https://github.com/n8n-io/n8n/issues/8242)) ([f293956](https://github.com/n8n-io/n8n/commit/f2939568cf399e67214e89bc7f859323aaeda8dd))
# [1.23.0](https://github.com/n8n-io/n8n/compare/n8n@1.22.0...n8n@1.23.0) (2024-01-03)
### Bug Fixes
* **Asana Node:** Omit body from GET, HEAD, and DELETE requests ([#8057](https://github.com/n8n-io/n8n/issues/8057)) ([15ffd4f](https://github.com/n8n-io/n8n/commit/15ffd4fb9f967302e2444a873a804d2ccb64e748))
* **core:** Better input validation for the changeRole endpoint ([#8189](https://github.com/n8n-io/n8n/issues/8189)) ([cfe9525](https://github.com/n8n-io/n8n/commit/cfe9525dd4e2dbf2496bd86ad854bb744b5dc8fe))
* **core:** Fix issue that pinnedData is not used with Test-Webhooks ([#8123](https://github.com/n8n-io/n8n/issues/8123)) ([fa8bd8b](https://github.com/n8n-io/n8n/commit/fa8bd8b9eb202989229028cb6975cd2b50e5eef9))
* **core:** Handle empty executions table in pruning in migrations ([#8121](https://github.com/n8n-io/n8n/issues/8121)) ([ffaa30d](https://github.com/n8n-io/n8n/commit/ffaa30ddc4ee312f44726c17a7ec91b5551092ad))
* **core:** Remove circular dependency in WorkflowService and ActiveWorkflowRunner ([#8128](https://github.com/n8n-io/n8n/issues/8128)) ([21788d9](https://github.com/n8n-io/n8n/commit/21788d9153fb730965dabbce64c50c3b929ee728)), closes [#8122](https://github.com/n8n-io/n8n/issues/8122)
* **core:** Use pinned data only for manual mode ([#8164](https://github.com/n8n-io/n8n/issues/8164)) ([ea7e76f](https://github.com/n8n-io/n8n/commit/ea7e76fa3b3dc1f37b0415e14ea5ff90b8017b9a))
* **Discord Node:** Remove unnecessary requirement on parameters ([#8060](https://github.com/n8n-io/n8n/issues/8060)) ([ef3a577](https://github.com/n8n-io/n8n/commit/ef3a57719eb42777502cafdd38009e6cb5b484ce))
* **editor:** Avoid sanitizing output to search node data ([#8126](https://github.com/n8n-io/n8n/issues/8126)) ([c83d9f4](https://github.com/n8n-io/n8n/commit/c83d9f45bab986eb930e9da69eec970d3a72263d))
* **editor:** Enable explicit undo keyboard shortcut across all code editors ([#8178](https://github.com/n8n-io/n8n/issues/8178)) ([cf7f668](https://github.com/n8n-io/n8n/commit/cf7f6688bac5dd31dc3a45df4ecce579939141e2)), closes [#5297](https://github.com/n8n-io/n8n/issues/5297)
* **editor:** Fix operation change failing in certain conditions ([#8114](https://github.com/n8n-io/n8n/issues/8114)) ([711fa2b](https://github.com/n8n-io/n8n/commit/711fa2b9251154b50d8e5e7cd9a857ccb2c0bec6)), closes [/github.com/n8n-io/n8n/blob/7806a65229878a473f5526bad0b94614e8bfa8aa/packages/workflow/src/NodeHelpers.ts#L786](https://github.com//github.com/n8n-io/n8n/blob/7806a65229878a473f5526bad0b94614e8bfa8aa/packages/workflow/src/NodeHelpers.ts/issues/L786)
* **editor:** Fix templates view layout ([#8196](https://github.com/n8n-io/n8n/issues/8196)) ([d01e42a](https://github.com/n8n-io/n8n/commit/d01e42a2aabedfd4c0f79799bbfc9b1a235d4233))
* **editor:** Fix UI urls when hosted behind a path prefix ([#8198](https://github.com/n8n-io/n8n/issues/8198)) ([5c078f1](https://github.com/n8n-io/n8n/commit/5c078f1b3d78c7038bfdbb083fd029ef61bf2dfc)), closes [#8061](https://github.com/n8n-io/n8n/issues/8061)
* **editor:** Prevent browser zoom when scrolling inside sticky edit mode ([#8116](https://github.com/n8n-io/n8n/issues/8116)) ([e928210](https://github.com/n8n-io/n8n/commit/e928210ccdc00ad8a38e3f96ba5145c35e7b007b))
* **editor:** Prevent canvas undo/redo when NDV is open ([#8118](https://github.com/n8n-io/n8n/issues/8118)) ([39e45d8](https://github.com/n8n-io/n8n/commit/39e45d8b929d474f1e7587329b003fe15b61636d))
* **editor:** Prevent storing pairedItem data inside of pinData ([#8173](https://github.com/n8n-io/n8n/issues/8173)) ([405e267](https://github.com/n8n-io/n8n/commit/405e26757e2591b42a4bfeedd1fea997fbbb08c9))
* **GitHub Node:** Fix issue that File->Get did not run once per item ([#8190](https://github.com/n8n-io/n8n/issues/8190)) ([11cda41](https://github.com/n8n-io/n8n/commit/11cda41214100a1a3e65309434ab8be3ccef1898))
* **Invoice Ninja Node:** Fix issue with custom invoice numbers not working with v5 ([#8200](https://github.com/n8n-io/n8n/issues/8200)) ([3b6ae2d](https://github.com/n8n-io/n8n/commit/3b6ae2d0a510a57b27fc1a44cb3e710e2a783800))
* **Microsoft Excel 365 Node:** Ensure arg is string during worksheet table search ([#8154](https://github.com/n8n-io/n8n/issues/8154)) ([8e873ca](https://github.com/n8n-io/n8n/commit/8e873ca2f3c73ddd7bbef2218d8da82032f66cec))
* **Notion Node:** Ensure arg is string during page ID extraction ([#8153](https://github.com/n8n-io/n8n/issues/8153)) ([e94b8a6](https://github.com/n8n-io/n8n/commit/e94b8a6c30aaa2e59117d5a0cc03e1590d7ea8ca))
* **Redis Trigger Node:** Activating a workflow with a Redis trigger fails ([#8129](https://github.com/n8n-io/n8n/issues/8129)) ([a169b74](https://github.com/n8n-io/n8n/commit/a169b7406279de43dbd3fd7d13166d987c60d01a))
* **Schedule Trigger Node:** Use the correct `moment` import ([#8185](https://github.com/n8n-io/n8n/issues/8185)) ([17a4e2e](https://github.com/n8n-io/n8n/commit/17a4e2ea80c664e248c136b7e66eef710ccba7f2)), closes [#8184](https://github.com/n8n-io/n8n/issues/8184)
* Show public API upgrade CTA when feature is not enabled ([#8109](https://github.com/n8n-io/n8n/issues/8109)) ([e9c7fd7](https://github.com/n8n-io/n8n/commit/e9c7fd73975ced504d5a16a0dbbc313f15ccd8ab))
### Features
* **core:** Add closeFunction support to Sub-Nodes ([#7708](https://github.com/n8n-io/n8n/issues/7708)) ([bec0fae](https://github.com/n8n-io/n8n/commit/bec0faed9e51fe6ea20ab3b07b4dfa849b28516b))
* **core:** Add user.profile.beforeUpdate hook ([#8144](https://github.com/n8n-io/n8n/issues/8144)) ([e126ed7](https://github.com/n8n-io/n8n/commit/e126ed74f3d9ed3dae72252cb8c9e8a6f7620808))
* **core:** Improvements/overhaul for nodes working with binary data ([#7651](https://github.com/n8n-io/n8n/issues/7651)) ([5e16dd4](https://github.com/n8n-io/n8n/commit/5e16dd4ab4457acf21d3d7a3566d07944ff7f857))
* **core:** Remove discontinued crypto-js ([#8104](https://github.com/n8n-io/n8n/issues/8104)) ([01e9a79](https://github.com/n8n-io/n8n/commit/01e9a79238bbd2c14ae77a12e54fc1c6f41e1246))
* **core:** Unify application components shutdown ([#8097](https://github.com/n8n-io/n8n/issues/8097)) ([3a881be](https://github.com/n8n-io/n8n/commit/3a881be6c25b3e16d8c53227dc851cb420f5f1bf))
* **editor:** Add node execution status indicator to output panel ([#8124](https://github.com/n8n-io/n8n/issues/8124)) ([ab74bad](https://github.com/n8n-io/n8n/commit/ab74bade05cb30e7fa65a491789a3df3ab7bf8b8))
* **editor:** Add template Id to workflow metadata ([#8088](https://github.com/n8n-io/n8n/issues/8088)) ([517b050](https://github.com/n8n-io/n8n/commit/517b050d0ae1a64987ac00d5795c5e59ed176f3f))
* **Home Assistant Node:** Use the new Home Assistant logo ([#8150](https://github.com/n8n-io/n8n/issues/8150)) ([518a99e](https://github.com/n8n-io/n8n/commit/518a99e5287dc648edafd34a4dd27c9205eb8629))
* **Qdrant Vector Store Node:** Qdrant vector store support ([#8080](https://github.com/n8n-io/n8n/issues/8080)) ([66460f6](https://github.com/n8n-io/n8n/commit/66460f66b0b02ae6f342e52500b29fe8b724e1dc))
* **Wordpress Node:** Add option to ignore error when using self signed certificates ([#8199](https://github.com/n8n-io/n8n/issues/8199)) ([65c8e12](https://github.com/n8n-io/n8n/commit/65c8e12b96ac8c1c53d3879d91982ca834f3cda1))
# [1.22.0](https://github.com/n8n-io/n8n/compare/n8n@1.21.0...n8n@1.22.0) (2023-12-21)
### Bug Fixes
* **ActiveCampaign Node:** Fix pagination issue when loading tags ([#8017](https://github.com/n8n-io/n8n/issues/8017)) ([1943857](https://github.com/n8n-io/n8n/commit/19438572312cf9354c333aeb52ccbf1ab81fc51f))
* **core:** Close db connection gracefully when exiting ([#8045](https://github.com/n8n-io/n8n/issues/8045)) ([e69707e](https://github.com/n8n-io/n8n/commit/e69707efd4bd947fdf6b9c66f373da63d34f41e9))
* **core:** Consider timeout in shutdown an error ([#8050](https://github.com/n8n-io/n8n/issues/8050)) ([4cae976](https://github.com/n8n-io/n8n/commit/4cae976a3b428bd528fe71ef0b240c0fd6e23bbf))
* **core:** Do not display error when stopping jobless execution in queue mode ([#8007](https://github.com/n8n-io/n8n/issues/8007)) ([8e6b951](https://github.com/n8n-io/n8n/commit/8e6b951a76e08b9ee9740fdd853f77553ad60cd6))
* **core:** Fix shutdown if terminating before hooks are initialized ([#8047](https://github.com/n8n-io/n8n/issues/8047)) ([6ae2f5e](https://github.com/n8n-io/n8n/commit/6ae2f5efea65e23029475ccdc5a65ec7c8152423))
* **core:** Handle multiple termination signals correctly ([#8046](https://github.com/n8n-io/n8n/issues/8046)) ([67bd8ad](https://github.com/n8n-io/n8n/commit/67bd8ad698bd0afe6ff7183d75da8bca4085598e))
* **core:** Initialize queue once in queue mode ([#8025](https://github.com/n8n-io/n8n/issues/8025)) ([53c0b49](https://github.com/n8n-io/n8n/commit/53c0b49d15047461e3b65baed65c9d76dff99539))
* **core:** Prevent axios from force setting a form-urlencoded content-type ([#8117](https://github.com/n8n-io/n8n/issues/8117)) ([bba9576](https://github.com/n8n-io/n8n/commit/bba95761e2f2b54af1fcab8a7b1d626ca10d537e)), closes [/github.com/axios/axios/blob/v1.x/lib/core/dispatchRequest.js#L45-L47](https://github.com//github.com/axios/axios/blob/v1.x/lib/core/dispatchRequest.js/issues/L45-L47)
* **core:** Remove circular references before serializing executions in public API ([#8043](https://github.com/n8n-io/n8n/issues/8043)) ([989888d](https://github.com/n8n-io/n8n/commit/989888d9bcec6f4eb3c811ce10d480737d96b102)), closes [#8030](https://github.com/n8n-io/n8n/issues/8030)
* **core:** Restore workflow ID during execution creation ([#8031](https://github.com/n8n-io/n8n/issues/8031)) ([c5e6ba8](https://github.com/n8n-io/n8n/commit/c5e6ba8cdd4a8f117ccc2e89e55497117156d8af)), closes [/github.com/n8n-io/n8n/pull/8002/files#diff-c8cbb62ca9ab2ae45e5f565cd8c63fff6475809a6241ea0b90acc575615224](https://github.com//github.com/n8n-io/n8n/pull/8002/files/issues/diff-c8cbb62ca9ab2ae45e5f565cd8c63fff6475809a6241ea0b90acc575615224)
* **core:** Use relative imports for dynamic imports in SecurityAuditService ([#8086](https://github.com/n8n-io/n8n/issues/8086)) ([785bf99](https://github.com/n8n-io/n8n/commit/785bf9974e38ea84c016e210a3108f4af567510d)), closes [#8085](https://github.com/n8n-io/n8n/issues/8085)
* **editor:** Add back credential `use` permission ([#8023](https://github.com/n8n-io/n8n/issues/8023)) ([329e5bf](https://github.com/n8n-io/n8n/commit/329e5bf9eed8556aba2bbd50bad9dbd6d3b373ad))
* **editor:** Cleanup Executions page component ([#8053](https://github.com/n8n-io/n8n/issues/8053)) ([2689c37](https://github.com/n8n-io/n8n/commit/2689c37e87c5b3ae5029121f4d3dc878841e8844))
* **editor:** Disable auto scroll and list size check when clicking on executions ([#7983](https://github.com/n8n-io/n8n/issues/7983)) ([fcb8b91](https://github.com/n8n-io/n8n/commit/fcb8b91f37e1fb0ef42f411c84390180e1ed7bbe))
* **editor:** Ensure execution data overrides pinned data when copying in executions view ([#8009](https://github.com/n8n-io/n8n/issues/8009)) ([1d1cb0d](https://github.com/n8n-io/n8n/commit/1d1cb0d3c530856e0c26d8f146f60b2555625ab6))
* **editor:** Fix copy/paste issue when switch node is in workflow ([#8103](https://github.com/n8n-io/n8n/issues/8103)) ([4b86926](https://github.com/n8n-io/n8n/commit/4b86926752fb1304a46385cb46bdf34fda0d53b6))
* **editor:** Make keyboard shortcuts more strict; don't accept extra Ctrl/Alt/Shift keys ([#8024](https://github.com/n8n-io/n8n/issues/8024)) ([8df49e1](https://github.com/n8n-io/n8n/commit/8df49e134d886267f9f7475573d013371220dcac))
* **editor:** Show credential share info only to appropriate users ([#8020](https://github.com/n8n-io/n8n/issues/8020)) ([b29b4d4](https://github.com/n8n-io/n8n/commit/b29b4d442bb0617aa516748ec48379eae0996cf0))
* **editor:** Turn off executions list auto-refresh after leaving the page ([#8005](https://github.com/n8n-io/n8n/issues/8005)) ([e3c363d](https://github.com/n8n-io/n8n/commit/e3c363d72cf4ee49086d012f92a7b34be958482f))
* **editor:** Update image sizes in template description not to be full width always ([#8037](https://github.com/n8n-io/n8n/issues/8037)) ([63a6e7e](https://github.com/n8n-io/n8n/commit/63a6e7e0340e1b00719f212ac620600a90d70ef1))
* **HTTP Request Node:** Do not create circular references in HTTP request node output ([#8030](https://github.com/n8n-io/n8n/issues/8030)) ([5b7ea16](https://github.com/n8n-io/n8n/commit/5b7ea16d9a20880c72779b02620e99ebe9f3617a))
* Stop binary data restoration from preventing execution from finishing ([#8082](https://github.com/n8n-io/n8n/issues/8082)) ([5ffff1b](https://github.com/n8n-io/n8n/commit/5ffff1bb22691c09c5ca8b3ada2a19d5ce155a0b))
* Upgrade axios to address CVE-2023-45857 ([#7713](https://github.com/n8n-io/n8n/issues/7713)) ([64eb9bb](https://github.com/n8n-io/n8n/commit/64eb9bbc3624ee8f2fa90812711ad568926fdca8))
### Features
* Add config option to prefer GET request over LIST when using Hashicorp Vault ([#8049](https://github.com/n8n-io/n8n/issues/8049)) ([439a22d](https://github.com/n8n-io/n8n/commit/439a22d68f7bf32f281b1078b71607307640a09b))
* Add option to `returnIntermediateSteps` for AI agents ([#8113](https://github.com/n8n-io/n8n/issues/8113)) ([7806a65](https://github.com/n8n-io/n8n/commit/7806a65229878a473f5526bad0b94614e8bfa8aa))
* **core:** Add N8N_GRACEFUL_SHUTDOWN_TIMEOUT env var ([#8068](https://github.com/n8n-io/n8n/issues/8068)) ([614f488](https://github.com/n8n-io/n8n/commit/614f48838626e2af8e3f2e76ee4a144af2d40f72))
* **editor:** Add lead enrichment suggestions to workflow list ([#8042](https://github.com/n8n-io/n8n/issues/8042)) ([36a923c](https://github.com/n8n-io/n8n/commit/36a923cf7bd4d42b8f8decbf01255c41d6dc1671)), closes [-update-workflows-list-page-to-show-fake-door-templates#comment-b6644c99](https://github.com/-update-workflows-list-page-to-show-fake-door-templates/issues/comment-b6644c99)
* **editor:** Finalize workers view ([#8052](https://github.com/n8n-io/n8n/issues/8052)) ([edfa784](https://github.com/n8n-io/n8n/commit/edfa78414d6bce901becc05e9d860f2521139688))
* **editor:** Gracefully ignore invalid payloads in postMessage handler ([#8096](https://github.com/n8n-io/n8n/issues/8096)) ([9d22c7a](https://github.com/n8n-io/n8n/commit/9d22c7a2782a1908f81bcf80260cd91cb296e239))
* **editor:** Upgrade frontend tooling to address a few vulnerabilities ([#8100](https://github.com/n8n-io/n8n/issues/8100)) ([19b7f1f](https://github.com/n8n-io/n8n/commit/19b7f1ffb17dcd6ac77839f97c2544f60f4ad55e))
* **Filter Node:** Overhaul UI by adding the new filter component ([#8016](https://github.com/n8n-io/n8n/issues/8016)) ([3d53052](https://github.com/n8n-io/n8n/commit/3d530522f828dfc985ae98e4bb551aa3a2bd44c6))
* **Respond to Webhook Node:** Overhaul with improvements like returning all items ([#8093](https://github.com/n8n-io/n8n/issues/8093)) ([32d397e](https://github.com/n8n-io/n8n/commit/32d397eff315fdc77677c0b134a7a25bcd8ca5d0))
### Performance Improvements
* **editor:** Improve canvas rendering performance ([#8022](https://github.com/n8n-io/n8n/issues/8022)) ([b780436](https://github.com/n8n-io/n8n/commit/b780436a6b445dc5951217b5a1f2c61b34961757))
# [1.21.0](https://github.com/n8n-io/n8n/compare/n8n@1.20.0...n8n@1.21.0) (2023-12-13)
### Bug Fixes
* **core:** Ensure inviter and invitee are set correctly in invite link ([#7943](https://github.com/n8n-io/n8n/issues/7943)) ([386bd61](https://github.com/n8n-io/n8n/commit/386bd619676e54e960ca0af3ff47fa3b9c16c813)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **core:** Fix user comparison in same-user subworkflow caller policy ([#7913](https://github.com/n8n-io/n8n/issues/7913)) ([92bab72](https://github.com/n8n-io/n8n/commit/92bab72cffb1083b495d211d0a31920e83e66769))
* **core:** Perform multi-main leader check against key ID ([#7964](https://github.com/n8n-io/n8n/issues/7964)) ([1a87f70](https://github.com/n8n-io/n8n/commit/1a87f70e8404218308072ee2f35c6ba2af34c23f)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **core:** Prevent workflow history saving error from happening ([#7812](https://github.com/n8n-io/n8n/issues/7812)) ([e5581ce](https://github.com/n8n-io/n8n/commit/e5581ce8023e21d3dcf140099f3a53e5ffb4584f))
* **editor:** Add missing string for worker in log streaming ([#7971](https://github.com/n8n-io/n8n/issues/7971)) ([148bc1d](https://github.com/n8n-io/n8n/commit/148bc1d303af3aafd73e73e11c3dd9cefd40a1dd)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **editor:** Allow SSH protocol in git repository URL for environments ([#7944](https://github.com/n8n-io/n8n/issues/7944)) ([bc1c72f](https://github.com/n8n-io/n8n/commit/bc1c72f992a47a9c263aec175ca820088cf340ec)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **editor:** Fix bug with node names with certain characters ([#8013](https://github.com/n8n-io/n8n/issues/8013)) ([26f0d57](https://github.com/n8n-io/n8n/commit/26f0d57f5fb71a06c92968a4997cceae62f32312))
* **editor:** Fix Webhook URL expansion icon ([#8011](https://github.com/n8n-io/n8n/issues/8011)) ([b00b905](https://github.com/n8n-io/n8n/commit/b00b9057a42f23cd9c4bb6675a3e6134610bf81b))
* **editor:** Prevent opening NDV search if `/` is typed in a contenteditable element ([#7968](https://github.com/n8n-io/n8n/issues/7968)) ([e8a493f](https://github.com/n8n-io/n8n/commit/e8a493f71863e6a5d2685b48a61a0d11daf5edc5))
* **editor:** Return early in ws message handler if no 'command' keyword is found ([#7946](https://github.com/n8n-io/n8n/issues/7946)) ([5b2defc](https://github.com/n8n-io/n8n/commit/5b2defc867a0627a861bf0fb98abfd99f8efe934))
* Ensure external hooks post workflow execute run in queue mode ([#7947](https://github.com/n8n-io/n8n/issues/7947)) ([3ba7deb](https://github.com/n8n-io/n8n/commit/3ba7deb337963d40ae70f40ffb2f4eb23cac89b7)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **FileMaker Node:** Prevent erroring on zero fields loaded ([#7955](https://github.com/n8n-io/n8n/issues/7955)) ([10ad386](https://github.com/n8n-io/n8n/commit/10ad3866048ad06d0e8455ed2c52c618ae9e5032)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* Fix issue preventing secrets from loading if the path contains - or / ([#7988](https://github.com/n8n-io/n8n/issues/7988)) ([0ac9594](https://github.com/n8n-io/n8n/commit/0ac959463f25187c5be4116a2209411afd903d87))
* **Google Sheets Node:** Prevent erroring on zero sheet search results ([#7957](https://github.com/n8n-io/n8n/issues/7957)) ([9b877a9](https://github.com/n8n-io/n8n/commit/9b877a942787c855c3a3a011c19c5d1d30b8da67)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **Google Sheets Node:** Prevent erroring when fetching mapping columns ([#7972](https://github.com/n8n-io/n8n/issues/7972)) ([29a1066](https://github.com/n8n-io/n8n/commit/29a10668d17cdeb8b0e93c912f59c5976b6fc6c6)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **Postgres Node:** Do not include id column in upsert fields selection if it's not unique ([#7975](https://github.com/n8n-io/n8n/issues/7975)) ([435392c](https://github.com/n8n-io/n8n/commit/435392cbfe150c5e85d092686b3b7e20273421cc))
* **Postgres Trigger Node:** Increase manual trigger timeout from 30 to 60 seconds ([#8015](https://github.com/n8n-io/n8n/issues/8015)) ([09a5729](https://github.com/n8n-io/n8n/commit/09a5729305a8072f5e98a320c85ad1c83a6946ed))
* Restrict updating/deleting of shared but not owned credentials ([#7950](https://github.com/n8n-io/n8n/issues/7950)) ([42e828d](https://github.com/n8n-io/n8n/commit/42e828d5c655e54b6a4ec83c398c684996b9cc3e)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **Webhook Node:** Binary data handling ([#7804](https://github.com/n8n-io/n8n/issues/7804)) ([565b409](https://github.com/n8n-io/n8n/commit/565b409a82ca6173efd19f26a5f5b27a359a3b87))
* **Webhook Node:** Do not create binary data when there is no data in the request ([#8000](https://github.com/n8n-io/n8n/issues/8000)) ([70f0755](https://github.com/n8n-io/n8n/commit/70f0755278e0a2bdb61c29623f27623b65473ab4)), closes [/github.com/n8n-io/n8n/pull/7804/files#r1422641833](https://github.com//github.com/n8n-io/n8n/pull/7804/files/issues/r1422641833)
### Features
* Add config option for external secret update interval ([#7995](https://github.com/n8n-io/n8n/issues/7995)) ([b6c1c04](https://github.com/n8n-io/n8n/commit/b6c1c04b541d0944c5baac1ab021539c8f020f10))
* AI nodes usability fixes + Summarization Chain V2 ([#7949](https://github.com/n8n-io/n8n/issues/7949)) ([dcf1286](https://github.com/n8n-io/n8n/commit/dcf12867b3c49596cd214812caee3292d2e794de)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* Data transformation nodes and actions in Nodes Panel ([#7760](https://github.com/n8n-io/n8n/issues/7760)) ([675ec21](https://github.com/n8n-io/n8n/commit/675ec21d335af2b2c9598bc2bec18194506ef71a))
* **editor:** Add AppCues tracking for onboarding event ([#7945](https://github.com/n8n-io/n8n/issues/7945)) ([04cabaf](https://github.com/n8n-io/n8n/commit/04cabafef7acbc30cba647732e2ca8ae8a02d29a)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **editor:** Add option to disable NDV in workflow previews ([#7990](https://github.com/n8n-io/n8n/issues/7990)) ([393afef](https://github.com/n8n-io/n8n/commit/393afef1747f168d5fa42be2424fd02125f1bbac)), closes [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **editor:** Filter component + implement in If node ([#7490](https://github.com/n8n-io/n8n/issues/7490)) ([8a53434](https://github.com/n8n-io/n8n/commit/8a5343401dd355436120a9a424ae455e80b50da6))
* **editor:** Show template credential setup based on feature flag ([#7989](https://github.com/n8n-io/n8n/issues/7989)) ([08ee307](https://github.com/n8n-io/n8n/commit/08ee3072093fb26b14b48e2b35d8c8d018317f13))
* **Google Ads Node:** Update to support v15 ([#7962](https://github.com/n8n-io/n8n/issues/7962)) ([7f01269](https://github.com/n8n-io/n8n/commit/7f0126915aae514a0ab515a4baf5582da2aeb1e3))
* Introduce advanced permissions ([#7844](https://github.com/n8n-io/n8n/issues/7844)) ([dbd62a4](https://github.com/n8n-io/n8n/commit/dbd62a4992ab8aca59e3cb50d3d970454e462238))
* **Local File Trigger Node:** Add polling option typically good to watch network files/folders ([#7942](https://github.com/n8n-io/n8n/issues/7942)) ([2fbdfec](https://github.com/n8n-io/n8n/commit/2fbdfec0c0a3f5da64764e7821e84db30b664e49))
* **n8n Form Trigger Node:** Improvements ([#7571](https://github.com/n8n-io/n8n/issues/7571)) ([953a58f](https://github.com/n8n-io/n8n/commit/953a58f18bfdd36fa8b526ca6213631aacab49cb))
# [1.20.0](https://github.com/n8n-io/n8n/compare/n8n@1.19.0...n8n@1.20.0) (2023-12-06)
### Bug Fixes
* **AWS DynamoDB Node:** Improve error message parsing ([#7793](https://github.com/n8n-io/n8n/issues/7793)) ([5ba5ed8](https://github.com/n8n-io/n8n/commit/5ba5ed8e3c8ba2f909859bde129d92576fbda46f))
* **core:** Allow grace period for binary data deletion after manual execution ([#7889](https://github.com/n8n-io/n8n/issues/7889)) ([61d8aeb](https://github.com/n8n-io/n8n/commit/61d8aebeaf6487269b252b353fdf16dcb67f41ff))
* **core:** Consolidate ownership and sharing data on workflows and credentials ([#7920](https://github.com/n8n-io/n8n/issues/7920)) ([38b88b9](https://github.com/n8n-io/n8n/commit/38b88b946bab67dc1a964bb3c980a627d4a32595))
* **core:** Fix hard deletes stopping if database query throws ([#7848](https://github.com/n8n-io/n8n/issues/7848)) ([46dd4d3](https://github.com/n8n-io/n8n/commit/46dd4d3105db3a15c81903ae81c9bbb21a45397b))
* **core:** Make sure mfa secret and recovery codes are not returned on login ([#7936](https://github.com/n8n-io/n8n/issues/7936)) ([f5502cc](https://github.com/n8n-io/n8n/commit/f5502cc628f6b348f7fe3325b96ec9dc3360beaf)), closes [/github.com/n8n-io/n8n/pull/6994/files#diff-95a87cb029a3d26e6722df2e68132453fc254fc1f4540cbdaa95cfdbda1893deL91](https://github.com//github.com/n8n-io/n8n/pull/6994/files/issues/diff-95a87cb029a3d26e6722df2e68132453fc254fc1f4540cbdaa95cfdbda1893deL91) [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **editor:** Fix deletion of last execution at execution preview ([#7883](https://github.com/n8n-io/n8n/issues/7883)) ([ce2d388](https://github.com/n8n-io/n8n/commit/ce2d388f059c0bb32d27f4b29e901d1a70083610))
* **editor:** Replace isInstanceOwner checks with scopes where applicable ([#7858](https://github.com/n8n-io/n8n/issues/7858)) ([132d691](https://github.com/n8n-io/n8n/commit/132d691cbf983f60293c7423de0077fb7c97e0af))
* **Google Sheets Node:** Fix issue with paired items not being set correctly ([#7862](https://github.com/n8n-io/n8n/issues/7862)) ([5207a2f](https://github.com/n8n-io/n8n/commit/5207a2fe5210e40d3b2aedd95182a18e497c72ab))
* **Notion Node:** Fix broken Notion node parameters ([#7864](https://github.com/n8n-io/n8n/issues/7864)) ([51d1f5b](https://github.com/n8n-io/n8n/commit/51d1f5b82070542d45c3d57387343959a3f0abb2)), closes [#7791](https://github.com/n8n-io/n8n/issues/7791)
### Features
* **BambooHR Node:** Add support for Only Current on company reports ([#7878](https://github.com/n8n-io/n8n/issues/7878)) ([4175801](https://github.com/n8n-io/n8n/commit/4175801c90ad4f744d1a7c331d4fb20891ed2e9e))
* **core:** Allow admin creation ([#7837](https://github.com/n8n-io/n8n/issues/7837)) ([476806e](https://github.com/n8n-io/n8n/commit/476806ebb0f31f656992fb67aba37116f10e1475))
* **editor:** Add sections to create node panel ([#7831](https://github.com/n8n-io/n8n/issues/7831)) ([39fa8d2](https://github.com/n8n-io/n8n/commit/39fa8d21bbee5d870b2620ec65401a5ca134c4f1))
* **editor:** Open template credential setup from collection ([#7882](https://github.com/n8n-io/n8n/issues/7882)) ([627ddb9](https://github.com/n8n-io/n8n/commit/627ddb91fb6c00796671a1f72f59a251cd89004d))
* **editor:** Select credentials in template setup if theres only one ([#7879](https://github.com/n8n-io/n8n/issues/7879)) ([fe3417a](https://github.com/n8n-io/n8n/commit/fe3417a615534a01cb0c7b5e8f47bc18abd5cd4d))
### Performance Improvements
* **editor:** Improve node rendering performance when opening large workflows ([#7904](https://github.com/n8n-io/n8n/issues/7904)) ([a8049a0](https://github.com/n8n-io/n8n/commit/a8049a0def21506ebf4fb1d3b69ae28ec35fdc21)), closes [#7901](https://github.com/n8n-io/n8n/issues/7901) [4#a39f9e5ba64a48b58a71d81c837e8227](https://github.com/4/issues/a39f9e5ba64a48b58a71d81c837e8227) [4#f6a177d32bde4b57ae2da0b8e454](https://github.com/4/issues/f6a177d32bde4b57ae2da0b8e454) [4#fef2d36ab02247e1a0f65a74f6fb534](https://github.com/4/issues/fef2d36ab02247e1a0f65a74f6fb534)
* **editor:** Improve performance when opening large workflows with node issues ([#7901](https://github.com/n8n-io/n8n/issues/7901)) ([4bd7ae2](https://github.com/n8n-io/n8n/commit/4bd7ae29f7c82b8817420e617a123024147c6c70))
# [1.19.0](https://github.com/n8n-io/n8n/compare/n8n@1.18.0...n8n@1.19.0) (2023-11-29)
### Bug Fixes
* **core:** Ensure member and admin cannot be promoted to owner ([#7830](https://github.com/n8n-io/n8n/issues/7830)) ([9b87a59](https://github.com/n8n-io/n8n/commit/9b87a596ca4aec462faedcca1ba4655b168bc3bc)), closes [/linear.app/n8n/issue/PAY-985/add-user-role-modification-endpoint#comment-62355f6](https://github.com//linear.app/n8n/issue/PAY-985/add-user-role-modification-endpoint/issues/comment-62355f6)
* **core:** Prevent error messages due to statistics about data loading ([#7824](https://github.com/n8n-io/n8n/issues/7824)) ([847f6ac](https://github.com/n8n-io/n8n/commit/847f6ac771316eea270d2e83adac5d8a6483475a))
* **core:** Tighten checks for multi-main setup usage ([#7788](https://github.com/n8n-io/n8n/issues/7788)) ([fdb2c18](https://github.com/n8n-io/n8n/commit/fdb2c18ecc49d1e8759e809d2e2c2e4aa17009da))
* **core:** Use AbortController to notify nodes to abort execution ([#6141](https://github.com/n8n-io/n8n/issues/6141)) ([d2c18c5](https://github.com/n8n-io/n8n/commit/d2c18c57274cac02e70cf1cc9e533a6ca98f0ec6))
* **editor:** Add telemetry to workflow history ([#7811](https://github.com/n8n-io/n8n/issues/7811)) ([d497041](https://github.com/n8n-io/n8n/commit/d4970410e1ba328b05ddc23abcbf33c719de5624))
* **editor:** Allow owners and admins to share workflows and credentials they don't own ([#7833](https://github.com/n8n-io/n8n/issues/7833)) ([3ab3ec9](https://github.com/n8n-io/n8n/commit/3ab3ec9da88f7b7ae07a98d7ef7c4f9892079048))
* **editor:** Disable context menu actions in read-only mode ([#7789](https://github.com/n8n-io/n8n/issues/7789)) ([902beff](https://github.com/n8n-io/n8n/commit/902beffce51d547094ea249d1fbbb70a879165d6))
* **editor:** Fix cloud plan data loading on instance ([#7841](https://github.com/n8n-io/n8n/issues/7841)) ([8b99384](https://github.com/n8n-io/n8n/commit/8b99384367161a47b3de13b7e83bcf6d07e3bf19))
* **editor:** Fix credential icon for old node type version ([#7843](https://github.com/n8n-io/n8n/issues/7843)) ([4074107](https://github.com/n8n-io/n8n/commit/40741075117dd8018ca1c6d03c050c3959142ebd))
* **editor:** Fix icon for unknown node type ([#7842](https://github.com/n8n-io/n8n/issues/7842)) ([28ac5a7](https://github.com/n8n-io/n8n/commit/28ac5a750eb28e9ab41611a76fa5fb9c30ce64dc))
* **editor:** Fix mouse position in workflow previews ([#7853](https://github.com/n8n-io/n8n/issues/7853)) ([c063398](https://github.com/n8n-io/n8n/commit/c0633987bfd6de24db0efc4bcb73adba9e9b6a74))
* **editor:** Show nice error when environment is not set up ([#7778](https://github.com/n8n-io/n8n/issues/7778)) ([5835e05](https://github.com/n8n-io/n8n/commit/5835e055d39921cdf2aa9799e427931bec8e5e2c))
* **editor:** Suppress dev server websocket messages in workflow view ([#7808](https://github.com/n8n-io/n8n/issues/7808)) ([685ffd7](https://github.com/n8n-io/n8n/commit/685ffd74137199f5e10145a33f3e0f03dabc2e7f))
* **Google Sheets Node:** Read operation execute for each item ([#7800](https://github.com/n8n-io/n8n/issues/7800)) ([d548872](https://github.com/n8n-io/n8n/commit/d5488725a83f6705b95c9de9d8736adf1b870134))
* **HTTP Request Node:** Enable expressions for binary input data fields ([#7782](https://github.com/n8n-io/n8n/issues/7782)) ([6208af0](https://github.com/n8n-io/n8n/commit/6208af07eb393b0fb8483b8ae4949a63423cc409))
* **Microsoft SQL Node:** Prevent double escaping table name ([#7801](https://github.com/n8n-io/n8n/issues/7801)) ([73ec753](https://github.com/n8n-io/n8n/commit/73ec7533ce4724940c2b23f677a9dfcf75de6a16))
### Features
* Add AI tool building capabilities ([#7336](https://github.com/n8n-io/n8n/issues/7336)) ([87def60](https://github.com/n8n-io/n8n/commit/87def60979bd6525b59df4bd811571a2afe83bec))
* Add initial scope checks via decorators ([#7737](https://github.com/n8n-io/n8n/issues/7737)) ([a37f1cb](https://github.com/n8n-io/n8n/commit/a37f1cb0bad87c486c387383f8179aa42f0b9e1a))
* Add user role select to users list settings page ([#7796](https://github.com/n8n-io/n8n/issues/7796)) ([137e238](https://github.com/n8n-io/n8n/commit/137e23853fdbd3e62037a6cb7f742811af41a03d))
* Ado 1296 spike credential setup in templates ([#7786](https://github.com/n8n-io/n8n/issues/7786)) ([aae45b0](https://github.com/n8n-io/n8n/commit/aae45b043b9e1427f9ffc44ef88d719782fccee5))
* **core:** Add Support for custom CORS origins for webhooks ([#7455](https://github.com/n8n-io/n8n/issues/7455)) ([99a9ea4](https://github.com/n8n-io/n8n/commit/99a9ea497a3d21739f911da5c88c076f60471bed))
* **core:** Allow user role modification ([#7797](https://github.com/n8n-io/n8n/issues/7797)) ([7a86d36](https://github.com/n8n-io/n8n/commit/7a86d3606852fcbc537533af24eef34279b229c6))
* **core:** Set up endpoint for all existing roles with license flag ([#7834](https://github.com/n8n-io/n8n/issues/7834)) ([2356fb0](https://github.com/n8n-io/n8n/commit/2356fb0f0c247271ffa00d1cf25460e06212f1c4))
* **editor:** Add node name and version to NDV node settings ([#7731](https://github.com/n8n-io/n8n/issues/7731)) ([da85198](https://github.com/n8n-io/n8n/commit/da851986f6f7cd4375b06c28a149dcb375fe8b83))
* **editor:** Add routing middleware, permission checks, RBAC store, RBAC component ([#7702](https://github.com/n8n-io/n8n/issues/7702)) ([67a8891](https://github.com/n8n-io/n8n/commit/67a88914f2f2d11c413e7f627d659333d8419af8))
* **editor:** Replace middleware for Role checks with Scope checks ([#7847](https://github.com/n8n-io/n8n/issues/7847)) ([72852a6](https://github.com/n8n-io/n8n/commit/72852a60eb15cbf45ebcdd390770c4cd9929a457))
* **editor:** Show avatars for users currently working on the same workflow ([#7763](https://github.com/n8n-io/n8n/issues/7763)) ([77bc8ec](https://github.com/n8n-io/n8n/commit/77bc8ecd4b1552f7253bc1348087db518ce7ce07))
* **Notion Node:** Option to simplify output in getChildBlocks operation ([#7791](https://github.com/n8n-io/n8n/issues/7791)) ([d667bca](https://github.com/n8n-io/n8n/commit/d667bca658a2b79fa5d0afba9ef25f26a10cdfc2))
* **Slack Node:** Add support for getting the profile of a user ([#7829](https://github.com/n8n-io/n8n/issues/7829)) ([90bb6ba](https://github.com/n8n-io/n8n/commit/90bb6ba4174a71f0d42e8dc9f009b879ca9d4616))
# [1.18.0](https://github.com/n8n-io/n8n/compare/n8n@1.17.0...n8n@1.18.0) (2023-11-22)
### Bug Fixes
* **core:** Account for non-ASCII chars in filename on binary data download ([#7742](https://github.com/n8n-io/n8n/issues/7742)) ([b4ebb1a](https://github.com/n8n-io/n8n/commit/b4ebb1a28dc87c297721299a635e836dcaa273b7))
* **core:** Correct permissions for getstatus ([#7724](https://github.com/n8n-io/n8n/issues/7724)) ([f96c1d2](https://github.com/n8n-io/n8n/commit/f96c1d204400028c55a2120d0569180379c0649f))
* **core:** Ensure failed executions are saved in queue mode ([#7744](https://github.com/n8n-io/n8n/issues/7744)) ([b7c5c74](https://github.com/n8n-io/n8n/commit/b7c5c7406f6f978bbd84737de34114e9492ae5f6))
* **core:** Guard against node not found on cancelling test webhook ([#7750](https://github.com/n8n-io/n8n/issues/7750)) ([6be453b](https://github.com/n8n-io/n8n/commit/6be453b716eff14df420ef565ea1b5ffb3ce73f0))
* **editor:** Handle permission edge cases (empty scopes) ([#7723](https://github.com/n8n-io/n8n/issues/7723)) ([e2ffd39](https://github.com/n8n-io/n8n/commit/e2ffd397fc0ab8d88128ba78d02c5df003af4a9d))
* **editor:** Make sure LineController is registered with chart.js ([#7730](https://github.com/n8n-io/n8n/issues/7730)) ([ebee1a5](https://github.com/n8n-io/n8n/commit/ebee1a590873aa56c23fd610616196ee27fe657a))
* **editor:** Move workerview entry into settings menu ([#7761](https://github.com/n8n-io/n8n/issues/7761)) ([366cd67](https://github.com/n8n-io/n8n/commit/366cd672a74649a19fc927e0327ae7c19ed5a1fc))
* **editor:** Only show push to git menu item to owners ([#7766](https://github.com/n8n-io/n8n/issues/7766)) ([0d3d33d](https://github.com/n8n-io/n8n/commit/0d3d33dd1f2354248ac341a0c9f2553812f404e0))
* **editor:** Show v1 banner dismiss button if owner ([#7722](https://github.com/n8n-io/n8n/issues/7722)) ([44d3b3e](https://github.com/n8n-io/n8n/commit/44d3b3ed7ee77715006591a4f49049388fcd4035))
* **editor:** Use project diagram icon for worker view ([#7764](https://github.com/n8n-io/n8n/issues/7764)) ([ff0b651](https://github.com/n8n-io/n8n/commit/ff0b6511f74831c499ab032910dfa9cf38356e8c))
* **editor:** Validate user info before submiting ([#7608](https://github.com/n8n-io/n8n/issues/7608)) ([2064f7f](https://github.com/n8n-io/n8n/commit/2064f7f251913a0cc22b4e27bb38df921f711109))
* **GitHub Node:** Fix issue preventing file edits on branches ([#7734](https://github.com/n8n-io/n8n/issues/7734)) ([ce002a6](https://github.com/n8n-io/n8n/commit/ce002a6cc672d1e13cc3d3470add78781d1ef20e))
* **Google Sheets Node:** Check for `null` before destructuring ([#7729](https://github.com/n8n-io/n8n/issues/7729)) ([5d4a52d](https://github.com/n8n-io/n8n/commit/5d4a52d3b7e35924e1a8c9a2c808418bdf224d2c))
* **Item Lists Node:** Don't check same type in remove duplicates operation ([#7678](https://github.com/n8n-io/n8n/issues/7678)) ([4f30764](https://github.com/n8n-io/n8n/commit/4f307646f3a5691331c7c610c62f562921a005f8))
* **JotForm Trigger Node:** Fix iteration on form loader ([#7751](https://github.com/n8n-io/n8n/issues/7751)) ([82f3202](https://github.com/n8n-io/n8n/commit/82f3202a2de2863f01abe3cf84d6f37eba4fb6fa))
### Features
* Add Creator hub link to Templates page ([#7721](https://github.com/n8n-io/n8n/issues/7721)) ([4dbae0e](https://github.com/n8n-io/n8n/commit/4dbae0e2e95d1b5f46cfc50a5a9fc6bb761defde))
* **core:** Coordinate manual workflow activation and deactivation in multi-main scenario ([#7643](https://github.com/n8n-io/n8n/issues/7643)) ([4c40825](https://github.com/n8n-io/n8n/commit/4c4082503c916d654758da738321f9e78a098ce5)), closes [#7566](https://github.com/n8n-io/n8n/issues/7566)
* **editor:** Add node context menu ([#7620](https://github.com/n8n-io/n8n/issues/7620)) ([8d12c1a](https://github.com/n8n-io/n8n/commit/8d12c1ad8d9283764647836bdd50224259d506e9))
* **editor:** Node IO filter ([#7503](https://github.com/n8n-io/n8n/issues/7503)) ([1881765](https://github.com/n8n-io/n8n/commit/18817651ec5d9ed5e774379ae5cf8f57c5461e43))
# [1.17.0](https://github.com/n8n-io/n8n/compare/n8n@1.16.0...n8n@1.17.0) (2023-11-15)
### Bug Fixes
* **Convert to/from binary data Node:** Better mime type defaults ([#7693](https://github.com/n8n-io/n8n/issues/7693)) ([9b3be0c](https://github.com/n8n-io/n8n/commit/9b3be0cfd8b0b58903d89ea3bf0b73be579a4f89))
* **core:** Consider subworkflows successfully run when in waiting state ([#7699](https://github.com/n8n-io/n8n/issues/7699)) ([0e00dab](https://github.com/n8n-io/n8n/commit/0e00dab9f5d5a6622cdc22fa8bfbecc039f6b67a))
* **core:** Fix named parameter resolution in migrations ([#7688](https://github.com/n8n-io/n8n/issues/7688)) ([4441ed5](https://github.com/n8n-io/n8n/commit/4441ed51169e8be930c548b17f54147ff6bd8e7d)), closes [#7628](https://github.com/n8n-io/n8n/issues/7628)
* **core:** Initialize JWT Secret before it's used anywhere ([#7707](https://github.com/n8n-io/n8n/issues/7707)) ([3460eb5](https://github.com/n8n-io/n8n/commit/3460eb5eeba95e51ccdac05084daf883c9750022))
* **core:** Reduce memory usage in credentials risk auditing ([#7663](https://github.com/n8n-io/n8n/issues/7663)) ([9fd6319](https://github.com/n8n-io/n8n/commit/9fd6319583d0446e41de4fb80d4bc5a6c5e1ca07))
* **Date & Time Node:** Add fromFormat option to solve ambiguous date strings ([#7675](https://github.com/n8n-io/n8n/issues/7675)) ([d2d11e0](https://github.com/n8n-io/n8n/commit/d2d11e0208e8a20145910bbdd02e7b273fb0aa13))
* **editor:** Fix resource mapper component being truncated ([#7664](https://github.com/n8n-io/n8n/issues/7664)) ([00dff50](https://github.com/n8n-io/n8n/commit/00dff50140d12e37bfeecdf1300ff313c179ec89))
* **editor:** More securely clear executions tab auto refresh timer ([#7685](https://github.com/n8n-io/n8n/issues/7685)) ([37dd658](https://github.com/n8n-io/n8n/commit/37dd658dc5bc1128c91d86105bf7f49dfcf96985))
* **editor:** Redirect to workflow editor after saving in debug mode ([#7645](https://github.com/n8n-io/n8n/issues/7645)) ([020042e](https://github.com/n8n-io/n8n/commit/020042ef1a329e805035061fbf6743bde892e3b1))
* **Google Sheets Node:** Append exceeding grid limits ([#7684](https://github.com/n8n-io/n8n/issues/7684)) ([88efb99](https://github.com/n8n-io/n8n/commit/88efb9958711bac446b6a698dfba50afd2b46132))
* **HTTP Request Node:** Support generic credentials when using pagination ([#7686](https://github.com/n8n-io/n8n/issues/7686)) ([48b240b](https://github.com/n8n-io/n8n/commit/48b240b0269858adb8fde8abb8a7211b2a3e78e0)), closes [#7653](https://github.com/n8n-io/n8n/issues/7653)
* **HubSpot Node:** Fetching available parameters fails when using expressions ([#7672](https://github.com/n8n-io/n8n/issues/7672)) ([a9ab738](https://github.com/n8n-io/n8n/commit/a9ab73896e6a42b2fd5df296c9ee95ac82936b7e))
* **HubSpot Node:** Update deal owner on Hubspot Deal ([#7673](https://github.com/n8n-io/n8n/issues/7673)) ([3c0734b](https://github.com/n8n-io/n8n/commit/3c0734bd2d92e9d2b9e99658c2e14710f57f36ef))
* **Spreadsheet File Node:** Read file as utf-8 in v1 ([#7701](https://github.com/n8n-io/n8n/issues/7701)) ([786b4ad](https://github.com/n8n-io/n8n/commit/786b4adcce910fa52104550d90a688c4046628f9))
### Features
* **core:** Expression function $ifEmpty ([#7660](https://github.com/n8n-io/n8n/issues/7660)) ([1c7225e](https://github.com/n8n-io/n8n/commit/1c7225ebdb1d92ce45313bbab27b0839d963fc4c))
* **Date & Time Node:** Option to include other fields in output item ([#7661](https://github.com/n8n-io/n8n/issues/7661)) ([aea3c50](https://github.com/n8n-io/n8n/commit/aea3c501313debaf1cf2b194023a534f829290ea))
* **Discord Node:** Overhaul ([#5351](https://github.com/n8n-io/n8n/issues/5351)) ([6a53c2a](https://github.com/n8n-io/n8n/commit/6a53c2a375ca71ffad1491da5ae7e6ec461a1a56))
* **Discourse Node:** Add new options to Get Users ([#7674](https://github.com/n8n-io/n8n/issues/7674)) ([2e8c841](https://github.com/n8n-io/n8n/commit/2e8c841277c2ba45ab2ab3e823135f2b15a7e570))
* **editor:** Add color selector to sticky node ([#7453](https://github.com/n8n-io/n8n/issues/7453)) ([8359364](https://github.com/n8n-io/n8n/commit/8359364536809e667be86f4b4df0838c94a801d7))
* **editor:** Add HTTP request nodes for credentials without a node ([#7157](https://github.com/n8n-io/n8n/issues/7157)) ([14035e1](https://github.com/n8n-io/n8n/commit/14035e1244fee5bc49b9afe57d63d9e887f25dd0))
* **editor:** Add workflow filters to querystring ([#7456](https://github.com/n8n-io/n8n/issues/7456)) ([afd637b](https://github.com/n8n-io/n8n/commit/afd637b5eab2bba33fd9ec8b24104bef5e2a4cc0))
* **editor:** Adds a EE view to show worker details and job status ([#7600](https://github.com/n8n-io/n8n/issues/7600)) ([cbc6909](https://github.com/n8n-io/n8n/commit/cbc690907fa36e2fde0218dd6f7737d00498c674))
* **GitLab Node:** Add support for pagination on getIssues ([#7529](https://github.com/n8n-io/n8n/issues/7529)) ([0a0798e](https://github.com/n8n-io/n8n/commit/0a0798e48500b0c159aa37deae7ce5d144f4f4c7))
* **OpenAI Node:** Add dall-e-3 support ([#7655](https://github.com/n8n-io/n8n/issues/7655)) ([a9c7188](https://github.com/n8n-io/n8n/commit/a9c7188c4d9d3a020cb26647c9030f6ffd47a35a))
* **RabbitMQ Trigger Node:** Add exchange and routing key options ([#7547](https://github.com/n8n-io/n8n/issues/7547)) ([5aee2b7](https://github.com/n8n-io/n8n/commit/5aee2b768f7743c6508c518bab35206577035380))
* **Telegram Node:** Add support for markdownv2 ([#7679](https://github.com/n8n-io/n8n/issues/7679)) ([819b3a7](https://github.com/n8n-io/n8n/commit/819b3a746a1cfbb785c97d0c681734211a599852))
* **Venafi TLS Protect Cloud Node:** Add region parameter to Venafi protect cloud ([#7689](https://github.com/n8n-io/n8n/issues/7689)) ([a08fca5](https://github.com/n8n-io/n8n/commit/a08fca51d928b7bfb7c0081287a38274048892bb))
### Performance Improvements
* **core:** Lazyload security audit reporters ([#7696](https://github.com/n8n-io/n8n/issues/7696)) ([b2ca050](https://github.com/n8n-io/n8n/commit/b2ca0500311d85742ef8abf8f9f0d1436e6d9ba1))
# [1.16.0](https://github.com/n8n-io/n8n/compare/n8n@1.15.1...n8n@1.16.0) (2023-11-08)
### Bug Fixes
* **core:** Comply with custom default for workflow saving settings ([#7634](https://github.com/n8n-io/n8n/issues/7634)) ([48c068f](https://github.com/n8n-io/n8n/commit/48c068f97b6c7df08fec9fd9d80a0e7eaacc95f5))
* **core:** Decrease reset password token expire time ([#7598](https://github.com/n8n-io/n8n/issues/7598)) ([2aa7f63](https://github.com/n8n-io/n8n/commit/2aa7f6375a01625980278aee714bdc06002b0948))
* **core:** Ensure `init` before checking leader or follower in multi-main scenario ([#7621](https://github.com/n8n-io/n8n/issues/7621)) ([a994ba5](https://github.com/n8n-io/n8n/commit/a994ba5e8d7092edeae05e7aa5fdfbb9fd854034))
* **core:** Ensure pruning starts only after migrations have completed ([#7626](https://github.com/n8n-io/n8n/issues/7626)) ([f748de9](https://github.com/n8n-io/n8n/commit/f748de9567ed1ecebea0ee35e9c71f8ea0e2d450))
* **core:** Fix accessor error when running partial execution ([#7618](https://github.com/n8n-io/n8n/issues/7618)) ([26361df](https://github.com/n8n-io/n8n/commit/26361dfcd31c9952c8ef109314ca88f5f03e40f4)), closes [#6229](https://github.com/n8n-io/n8n/issues/6229)
* **core:** Make password-reset urls valid only for single-use ([#7622](https://github.com/n8n-io/n8n/issues/7622)) ([6031424](https://github.com/n8n-io/n8n/commit/60314248f4b021f451eb744184fe150ddc03bc6e))
* **Crypto Node:** Fix issue with value not appearing for Sign action ([#7619](https://github.com/n8n-io/n8n/issues/7619)) ([5df583f](https://github.com/n8n-io/n8n/commit/5df583f783731e46500600e6a23ff3b7fdfb4e52))
* **editor:** Allow overriding theme from query params ([#7591](https://github.com/n8n-io/n8n/issues/7591)) ([2854a0c](https://github.com/n8n-io/n8n/commit/2854a0cf467258c6dacc15c2b200cf6480b6ecef))
* **editor:** Fix issue that frontend breaks with unkown nodes ([#7596](https://github.com/n8n-io/n8n/issues/7596)) ([db56a9e](https://github.com/n8n-io/n8n/commit/db56a9ee37e8b041ea8958fc8400b9e5b6b81316))
* **editor:** Fix local storage flags defaulting to undefined string ([#7603](https://github.com/n8n-io/n8n/issues/7603)) ([151e60f](https://github.com/n8n-io/n8n/commit/151e60f829663e79982aae6ac1cd8489f3083224))
* **editor:** Fix workflow history prune time limit (getting hours instead of days) ([#7644](https://github.com/n8n-io/n8n/issues/7644)) ([3d5a485](https://github.com/n8n-io/n8n/commit/3d5a485bcf7fef4c6b7d96df3a77c041178951a6))
* **editor:** Hide not supported node options ([#7597](https://github.com/n8n-io/n8n/issues/7597)) ([b532a7b](https://github.com/n8n-io/n8n/commit/b532a7bdb7d33d5ffb20665dfde58cb664d39b4a))
* **editor:** Remove unknown credentials on pasting workflow ([#7582](https://github.com/n8n-io/n8n/issues/7582)) ([d633753](https://github.com/n8n-io/n8n/commit/d63375368713b31e15735721c7a7603fe08a6645))
* **editor:** Reset canvas zoom before workspace reset in node view ([#7625](https://github.com/n8n-io/n8n/issues/7625)) ([78b84af](https://github.com/n8n-io/n8n/commit/78b84af8d1cfed005c7d9c715d832e8c91fd9e3f))
* **editor:** Zoom in/out on canvas the same amount on scroll/gesture ([#7602](https://github.com/n8n-io/n8n/issues/7602)) ([c92402a](https://github.com/n8n-io/n8n/commit/c92402a3cabfdc227f3c929bc7731d42f4516776))
* **Facebook Lead Ads Trigger Node:** Fix issue with missing scope for business management ([#7616](https://github.com/n8n-io/n8n/issues/7616)) ([32b85ba](https://github.com/n8n-io/n8n/commit/32b85ba2fec6e74d8648be7e718b52140c1bc4fc))
### Features
* **core:** Add the node version to telemetry in node_graph_string ([#7449](https://github.com/n8n-io/n8n/issues/7449)) ([59dc36a](https://github.com/n8n-io/n8n/commit/59dc36abd9141a863cb41c17a9115410b27bdb16))
* **core:** Coordinate workflow activation in multiple main scenario in internal API ([#7566](https://github.com/n8n-io/n8n/issues/7566)) ([c857e42](https://github.com/n8n-io/n8n/commit/c857e42677ef0d415caf66f00d7af029546dfd79))
* **core:** Initial support for two-way communication over websockets ([#7570](https://github.com/n8n-io/n8n/issues/7570)) ([ac87701](https://github.com/n8n-io/n8n/commit/ac877014eda83eb2ee61c87f29e2583f3fbfd125))
* **core:** Log executed migrations with info level ([#7586](https://github.com/n8n-io/n8n/issues/7586)) ([7dac9ab](https://github.com/n8n-io/n8n/commit/7dac9ab82c2f91cfbb66a57f175c1865e8c8107a))
* **core:** Rate limit forgot password endpoint ([#7604](https://github.com/n8n-io/n8n/issues/7604)) ([5790e25](https://github.com/n8n-io/n8n/commit/5790e251b8072679d7c061e2d2fa1f4229e03cf8))
* **LinkedIn Node:** Add support for Article thumbnails ([#7489](https://github.com/n8n-io/n8n/issues/7489)) ([e6d3d1a](https://github.com/n8n-io/n8n/commit/e6d3d1a4c2dd6a860e935df4b0ce3f13e23030c7))
* **NocoDB Node:** Add new data apis and workspace support ([#7329](https://github.com/n8n-io/n8n/issues/7329)) ([da2d2a8](https://github.com/n8n-io/n8n/commit/da2d2a83bbfb05db3a10aef99bfde3ccaf160d60))
## [1.15.1](https://github.com/n8n-io/n8n/compare/n8n@1.14.0...n8n@1.15.1) (2023-11-02)
### Bug Fixes
* **core:** Ensure execution deletion in worker lifecycle hook ([#7481](https://github.com/n8n-io/n8n/issues/7481)) ([742c8a8](https://github.com/n8n-io/n8n/commit/742c8a8534098522fe103fad09fa95f70c460b3d))
* **core:** Fix data encryption on credentials import ([#7560](https://github.com/n8n-io/n8n/issues/7560)) ([b350568](https://github.com/n8n-io/n8n/commit/b350568505d48ec880fe98d2b62ef090d5399c5f))
* **core:** Fix issue that prevents owner logging in when using ldap ([#7408](https://github.com/n8n-io/n8n/issues/7408)) ([479f902](https://github.com/n8n-io/n8n/commit/479f90231d0a03c69b17189384812b5a1d81ef3d))
* **core:** Handle missing resultData in runData ([#7523](https://github.com/n8n-io/n8n/issues/7523)) ([1055bd3](https://github.com/n8n-io/n8n/commit/1055bd3762b90b013a300bd87e3fa902e902cb9e))
* **core:** Permission check for subworkflow properly checking for workflow settings ([#7576](https://github.com/n8n-io/n8n/issues/7576)) ([437c95e](https://github.com/n8n-io/n8n/commit/437c95e84e144cc77f2866a74d6b670c415895cd))
* **core:** Prevent executions from becoming forever running ([#7569](https://github.com/n8n-io/n8n/issues/7569)) ([9bdb85c](https://github.com/n8n-io/n8n/commit/9bdb85c4ced96fde75435e334dc757d6c7679926))
* **core:** Upgrade crypto-js to address CVE-2023-46233 ([#7519](https://github.com/n8n-io/n8n/issues/7519)) ([65e5593](https://github.com/n8n-io/n8n/commit/65e559323371e8235b92e2134d9908d69043fac4))
* **editor:** Do not truncate form inputs ([#7528](https://github.com/n8n-io/n8n/issues/7528)) ([ae616f1](https://github.com/n8n-io/n8n/commit/ae616f146bc2ce8d37f8cf5116c6c4c8682a91a6))
* **editor:** Fix NDV close after using input select ([#7544](https://github.com/n8n-io/n8n/issues/7544)) ([3b5e181](https://github.com/n8n-io/n8n/commit/3b5e181e66f8d7e3860e3078dae7cbb20e92551a))
* **editor:** Fix NDV unexpected re-render ([#7532](https://github.com/n8n-io/n8n/issues/7532)) ([2853fcf](https://github.com/n8n-io/n8n/commit/2853fcff735fd0b98c19c1192349ef2c659d2493))
* **editor:** Fix route component caching, incorrect use of array reduce method and enable WF history feature ([#7434](https://github.com/n8n-io/n8n/issues/7434)) ([12a89e6](https://github.com/n8n-io/n8n/commit/12a89e6d1441f81380d5e477274a5e2d3eb29f2d))
* **editor:** Fixes the issue that Switch Node can not be created ([#7516](https://github.com/n8n-io/n8n/issues/7516)) ([df89685](https://github.com/n8n-io/n8n/commit/df89685e1548219f4c06614287abafbc96697817))
* **editor:** Handle `localStorage` being blocked/unavailable ([#7348](https://github.com/n8n-io/n8n/issues/7348)) ([c05bc67](https://github.com/n8n-io/n8n/commit/c05bc6728d3227af4931ddcda5ed8bc6a3539dd0))
* Fix dark mode small issues ([#7573](https://github.com/n8n-io/n8n/issues/7573)) ([1d81afc](https://github.com/n8n-io/n8n/commit/1d81afcbdf17166f3ebf468673e3ba348ae7fecb))
* **Jira Software Node:** Handle missing issue types in issue types loader ([#7534](https://github.com/n8n-io/n8n/issues/7534)) ([9762705](https://github.com/n8n-io/n8n/commit/9762705833c809fd2781de179279a15c1be988eb))
* **Switch Node:** Allow sortable Switch rules ([#7555](https://github.com/n8n-io/n8n/issues/7555)) ([7a56e58](https://github.com/n8n-io/n8n/commit/7a56e58a608132ef795d8c5cdaccb8caa49c0e8f))
### Features
* **core:** Add optional Error-Output ([#7460](https://github.com/n8n-io/n8n/issues/7460)) ([655efea](https://github.com/n8n-io/n8n/commit/655efeaf669e9722895b66fef47f000507459210))
* **core:** Make queue mode settings configurable ([#7526](https://github.com/n8n-io/n8n/issues/7526)) ([3d95b24](https://github.com/n8n-io/n8n/commit/3d95b243e935e4eba97a418d05fa687169ab7d07))
* **core:** Set up leader selection for multiple main instances ([#7527](https://github.com/n8n-io/n8n/issues/7527)) ([442c73e](https://github.com/n8n-io/n8n/commit/442c73e63bb54f50657a511d88912a80cab64c7f))
* **editor:** Implement the `UserStack` design system component ([#7559](https://github.com/n8n-io/n8n/issues/7559)) ([ce14f62](https://github.com/n8n-io/n8n/commit/ce14f6266b30caadb477b08d4257b82c769a74c3))
* **HTTP Request Node:** Add pagination support ([#5993](https://github.com/n8n-io/n8n/issues/5993)) ([cc2bd2e](https://github.com/n8n-io/n8n/commit/cc2bd2e19c8b75320b236de215d389220fbe24ae))
* **HTTP Request Node:** Update icon and default color ([#7572](https://github.com/n8n-io/n8n/issues/7572)) ([ff279ab](https://github.com/n8n-io/n8n/commit/ff279ab4112435c341b84081d68b976ff03bf261))
* **n8n Form Trigger Node:** Add text area and password input types ([#7474](https://github.com/n8n-io/n8n/issues/7474)) ([b72040a](https://github.com/n8n-io/n8n/commit/b72040aa5423aa6cb16dea2e7c6ea6439376b653))
* **editor:** Dark mode is here! You can change it under personal settings.([#6980](https://github.com/n8n-io/n8n/pull/6980)) ([0746783](https://github.com/n8n-io/n8n/commit/0746783e027ebe6715588a68db399a34e0211a96))
# [1.15.0](https://github.com/n8n-io/n8n/compare/n8n@1.14.0...n8n@1.15.0) (2023-11-02)
### Bug Fixes
* **core:** Ensure execution deletion in worker lifecycle hook ([#7481](https://github.com/n8n-io/n8n/issues/7481)) ([742c8a8](https://github.com/n8n-io/n8n/commit/742c8a8534098522fe103fad09fa95f70c460b3d))
* **core:** Fix data encryption on credentials import ([#7560](https://github.com/n8n-io/n8n/issues/7560)) ([b350568](https://github.com/n8n-io/n8n/commit/b350568505d48ec880fe98d2b62ef090d5399c5f))
* **core:** Fix issue that prevents owner logging in when using ldap ([#7408](https://github.com/n8n-io/n8n/issues/7408)) ([479f902](https://github.com/n8n-io/n8n/commit/479f90231d0a03c69b17189384812b5a1d81ef3d))
* **core:** Handle missing resultData in runData ([#7523](https://github.com/n8n-io/n8n/issues/7523)) ([1055bd3](https://github.com/n8n-io/n8n/commit/1055bd3762b90b013a300bd87e3fa902e902cb9e))
* **core:** Permission check for subworkflow properly checking for workflow settings ([#7576](https://github.com/n8n-io/n8n/issues/7576)) ([437c95e](https://github.com/n8n-io/n8n/commit/437c95e84e144cc77f2866a74d6b670c415895cd))
* **core:** Prevent executions from becoming forever running ([#7569](https://github.com/n8n-io/n8n/issues/7569)) ([9bdb85c](https://github.com/n8n-io/n8n/commit/9bdb85c4ced96fde75435e334dc757d6c7679926))
* **core:** Upgrade crypto-js to address CVE-2023-46233 ([#7519](https://github.com/n8n-io/n8n/issues/7519)) ([65e5593](https://github.com/n8n-io/n8n/commit/65e559323371e8235b92e2134d9908d69043fac4))
* **editor:** Do not truncate form inputs ([#7528](https://github.com/n8n-io/n8n/issues/7528)) ([ae616f1](https://github.com/n8n-io/n8n/commit/ae616f146bc2ce8d37f8cf5116c6c4c8682a91a6))
* **editor:** Fix NDV close after using input select ([#7544](https://github.com/n8n-io/n8n/issues/7544)) ([3b5e181](https://github.com/n8n-io/n8n/commit/3b5e181e66f8d7e3860e3078dae7cbb20e92551a))
* **editor:** Fix NDV unexpected re-render ([#7532](https://github.com/n8n-io/n8n/issues/7532)) ([2853fcf](https://github.com/n8n-io/n8n/commit/2853fcff735fd0b98c19c1192349ef2c659d2493))
* **editor:** Fix route component caching, incorrect use of array reduce method and enable WF history feature ([#7434](https://github.com/n8n-io/n8n/issues/7434)) ([12a89e6](https://github.com/n8n-io/n8n/commit/12a89e6d1441f81380d5e477274a5e2d3eb29f2d))
* **editor:** Fixes the issue that Switch Node can not be created ([#7516](https://github.com/n8n-io/n8n/issues/7516)) ([df89685](https://github.com/n8n-io/n8n/commit/df89685e1548219f4c06614287abafbc96697817))
* **editor:** Handle `localStorage` being blocked/unavailable ([#7348](https://github.com/n8n-io/n8n/issues/7348)) ([c05bc67](https://github.com/n8n-io/n8n/commit/c05bc6728d3227af4931ddcda5ed8bc6a3539dd0))
* Fix dark mode small issues ([#7573](https://github.com/n8n-io/n8n/issues/7573)) ([1d81afc](https://github.com/n8n-io/n8n/commit/1d81afcbdf17166f3ebf468673e3ba348ae7fecb))
* **Jira Software Node:** Handle missing issue types in issue types loader ([#7534](https://github.com/n8n-io/n8n/issues/7534)) ([9762705](https://github.com/n8n-io/n8n/commit/9762705833c809fd2781de179279a15c1be988eb))
* **Switch Node:** Allow sortable Switch rules ([#7555](https://github.com/n8n-io/n8n/issues/7555)) ([7a56e58](https://github.com/n8n-io/n8n/commit/7a56e58a608132ef795d8c5cdaccb8caa49c0e8f))
### Features
* **core:** Add optional Error-Output ([#7460](https://github.com/n8n-io/n8n/issues/7460)) ([655efea](https://github.com/n8n-io/n8n/commit/655efeaf669e9722895b66fef47f000507459210))
* **core:** Make queue mode settings configurable ([#7526](https://github.com/n8n-io/n8n/issues/7526)) ([3d95b24](https://github.com/n8n-io/n8n/commit/3d95b243e935e4eba97a418d05fa687169ab7d07))
* **core:** Set up leader selection for multiple main instances ([#7527](https://github.com/n8n-io/n8n/issues/7527)) ([442c73e](https://github.com/n8n-io/n8n/commit/442c73e63bb54f50657a511d88912a80cab64c7f))
* **editor:** Implement the `UserStack` design system component ([#7559](https://github.com/n8n-io/n8n/issues/7559)) ([ce14f62](https://github.com/n8n-io/n8n/commit/ce14f6266b30caadb477b08d4257b82c769a74c3))
* **HTTP Request Node:** Add pagination support ([#5993](https://github.com/n8n-io/n8n/issues/5993)) ([cc2bd2e](https://github.com/n8n-io/n8n/commit/cc2bd2e19c8b75320b236de215d389220fbe24ae))
* **HTTP Request Node:** Update icon and default color ([#7572](https://github.com/n8n-io/n8n/issues/7572)) ([ff279ab](https://github.com/n8n-io/n8n/commit/ff279ab4112435c341b84081d68b976ff03bf261))
* **n8n Form Trigger Node:** Add text area and password input types ([#7474](https://github.com/n8n-io/n8n/issues/7474)) ([b72040a](https://github.com/n8n-io/n8n/commit/b72040aa5423aa6cb16dea2e7c6ea6439376b653))
* * **editor:** Dark mode is here! You can change it under personal settings.([#6980](https://github.com/n8n-io/n8n/pull/6980)) ([0746783](https://github.com/n8n-io/n8n/commit/0746783e027ebe6715588a68db399a34e0211a96))
# [1.14.0](https://github.com/n8n-io/n8n/compare/n8n@1.13.0...n8n@1.14.0) (2023-10-25) # [1.14.0](https://github.com/n8n-io/n8n/compare/n8n@1.13.0...n8n@1.14.0) (2023-10-25)

View file

@ -1,50 +0,0 @@
paths:
'packages/**':
- If fixing bug, added test to cover scenario.
- If addressing forum or Github issue, added link to description.
'packages/**/*.ts':
- Added unit tests to cover new or updated functionality.
'**/*.vue':
- Used composition API for all new components.
- Added component or unit tests to cover functionality.
# cli
'packages/cli/src/databases/migrations/**':
- Requested review from at least two engineers on migration.
- Avoided irreversible data migrations.
- Avoided deleting or updating data keys.
- Wrote 'down' migration if possible.
'n8n/packages/cli/src/api/**':
- Added integration tests for new endpoints.
# editor ui
'packages/editor-ui/**/*.vue':
- Added E2E if adding new features.
- Used design system tokens (colors, spacings...) where possible.
'packages/editor-ui/src/mixins/restApi.ts':
- Avoided adding new methods. Only deleted from here.
'packages/editor-ui/src/mixins/**':
- Avoided adding new mixins (use composables instead). Only removed code from here.
'packages/editor-ui/src/views/NodeView.vue':
- Avoided adding code here. Only refactored to make it smaller.
'packages/editor-ui/src/hooks/**':
- Avoided adding new hooks. Only refactored to move hooks to relevant store instead.
# nodes-base
'packages/nodes-base/nodes/**':
- Added workflow tests for nodes if possible.
'packages/nodes-base/package.json':
- Avoided adding dependencies for nodes if not absolutely necessary.
# design-system
'packages/design-system/**/*.vue':
- Used design system tokens (colors, spacings...) where possible.
- Updated Storybook with new component or updated functionality.
# e2e
'cypress/e2e/**':
- Avoided chaining commands more than two or three times (to avoid flakiness because only last one will be retried).
- Spoofed endpoints that are not critical for the test (to avoid flakiness).
- Picked most efficient path to start the test (for example skipped account setup and starting at /workflow/new for a canvas test).
- Avoided adding waits on time (use request intercepts instead).
- Ensured each spec does not depend on any another spec to pass.

View file

@ -19,9 +19,12 @@ Great that you are here and you want to contribute to n8n
- [Start](#start) - [Start](#start)
- [Development cycle](#development-cycle) - [Development cycle](#development-cycle)
- [Test suite](#test-suite) - [Test suite](#test-suite)
- [Unit tests](#unit-tests)
- [E2E tests](#e2e-tests)
- [Releasing](#releasing) - [Releasing](#releasing)
- [Create custom nodes](#create-custom-nodes) - [Create custom nodes](#create-custom-nodes)
- [Extend documentation](#extend-documentation) - [Extend documentation](#extend-documentation)
- [Contribute workflow templates](#contribute-workflow-templates)
- [Contributor License Agreement](#contributor-license-agreement) - [Contributor License Agreement](#contributor-license-agreement)
## Code of conduct ## Code of conduct
@ -186,7 +189,9 @@ automatically build your code, restart the backend and refresh the frontend
### Test suite ### Test suite
The tests can be started via: #### Unit tests
Unit tests can be started via:
``` ```
pnpm test pnpm test
@ -196,6 +201,16 @@ If that gets executed in one of the package folders it will only run the tests
of this package. If it gets executed in the n8n-root folder it will run all of this package. If it gets executed in the n8n-root folder it will run all
tests of all packages. tests of all packages.
#### E2E tests
E2E tests can be started via one of the following commands:
- `pnpm test:e2e:ui`: Start n8n and run e2e tests interactively using built UI code. Does not react to code changes (i.e. runs `pnpm start` and `cypress open`)
- `pnpm test:e2e:dev`: Start n8n in development mode and run e2e tests interactively. Reacts to code changes (i.e. runs `pnpm dev` and `cypress open`)
- `pnpm test:e2e:all`: Start n8n and run e2e tests headless (i.e. runs `pnpm start` and `cypress run --headless`)
⚠️ Remember to stop your dev server before. Otherwise port binding will fail.
## Releasing ## Releasing
To start a release, trigger [this workflow](https://github.com/n8n-io/n8n/actions/workflows/release-create-pr.yml) with the SemVer release type, and select a branch to cut this release from. This workflow will then: To start a release, trigger [this workflow](https://github.com/n8n-io/n8n/actions/workflows/release-create-pr.yml) with the SemVer release type, and select a branch to cut this release from. This workflow will then:
@ -220,6 +235,14 @@ Learn about [building nodes](https://docs.n8n.io/integrations/creating-nodes/) t
The repository for the n8n documentation on [docs.n8n.io](https://docs.n8n.io) can be found [here](https://github.com/n8n-io/n8n-docs). The repository for the n8n documentation on [docs.n8n.io](https://docs.n8n.io) can be found [here](https://github.com/n8n-io/n8n-docs).
## Contribute workflow templates
You can submit your workflows to n8n's template library.
n8n is working on a creator program, and developing a marketplace of templates. This is an ongoing project, and details are likely to change.
Refer to [n8n Creator hub](https://www.notion.so/n8n/n8n-Creator-hub-7bd2cbe0fce0449198ecb23ff4a2f76f) for information on how to submit templates and become a creator.
## Contributor License Agreement ## Contributor License Agreement
That we do not have any potential problems later it is sadly necessary to sign a [Contributor License Agreement](CONTRIBUTOR_LICENSE_AGREEMENT.md). That can be done literally with the push of a button. That we do not have any potential problems later it is sadly necessary to sign a [Contributor License Agreement](CONTRIBUTOR_LICENSE_AGREEMENT.md). That can be done literally with the push of a button.

View file

@ -2,7 +2,7 @@
# n8n - Workflow automation tool # n8n - Workflow automation tool
n8n is an extendable workflow automation tool. With a [fair-code](http://faircode.io) distribution model, n8n n8n is an extendable workflow automation tool. With a [fair-code](https://faircode.io) distribution model, n8n
will always have visible source code, be available to self-host, and allow you to add your own custom will always have visible source code, be available to self-host, and allow you to add your own custom
functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect functions, logic and apps. n8n's node-based approach makes it highly versatile, enabling you to connect
anything to everything. anything to everything.
@ -94,7 +94,7 @@ development environment ready in minutes.
## License ## License
n8n is [fair-code](http://faircode.io) distributed under the n8n is [fair-code](https://faircode.io) distributed under the
[**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) and the [**Sustainable Use License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md) and the
[**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE_EE.md). [**n8n Enterprise License**](https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE_EE.md).

View file

@ -19,4 +19,9 @@ module.exports = defineConfig({
experimentalInteractiveRunEvents: true, experimentalInteractiveRunEvents: true,
experimentalSessionAndOrigin: true, experimentalSessionAndOrigin: true,
}, },
env: {
MAX_PINNED_DATA_SIZE: process.env.VUE_APP_MAX_PINNED_DATA_SIZE
? parseInt(process.env.VUE_APP_MAX_PINNED_DATA_SIZE, 10)
: 16 * 1024,
},
}); });

View file

@ -0,0 +1,12 @@
export const overrideFeatureFlag = (name: string, value: boolean | string) => {
cy.window().then((win) => {
// If feature flags hasn't been initialized yet, we store the override
// in local storage and it gets loaded when the feature flags are
// initialized.
win.localStorage.setItem('N8N_EXPERIMENT_OVERRIDES', JSON.stringify({ [name]: value }));
if (win.featureFlags) {
win.featureFlags.override(name, value);
}
});
};

View file

@ -0,0 +1,48 @@
/**
* Getters
*/
export function getManualChatModal() {
return cy.getByTestId('lmChat-modal');
}
export function getManualChatInput() {
return cy.getByTestId('workflow-chat-input');
}
export function getManualChatSendButton() {
return getManualChatModal().getByTestId('workflow-chat-send-button');
}
export function getManualChatMessages() {
return getManualChatModal().get('.messages .message');
}
export function getManualChatModalCloseButton() {
return getManualChatModal().get('.el-dialog__close');
}
export function getManualChatModalLogs() {
return getManualChatModal().getByTestId('lm-chat-logs');
}
export function getManualChatModalLogsTree() {
return getManualChatModalLogs().getByTestId('lm-chat-logs-tree');
}
export function getManualChatModalLogsEntries() {
return getManualChatModalLogs().getByTestId('lm-chat-logs-entry');
}
/**
* Actions
*/
export function sendManualChatMessage(message: string) {
getManualChatInput().type(message);
getManualChatSendButton().click();
}
export function closeManualChatModal() {
getManualChatModalCloseButton().click();
}

View file

@ -0,0 +1,54 @@
/**
* Getters
*/
export function getCredentialConnectionParameterInputs() {
return cy.getByTestId('credential-connection-parameter');
}
export function getCredentialConnectionParameterInputByName(name: string) {
return cy.getByTestId(`parameter-input-${name}`);
}
export function getEditCredentialModal() {
return cy.getByTestId('editCredential-modal', { timeout: 5000 });
}
export function getCredentialSaveButton() {
return cy.getByTestId('credential-save-button', { timeout: 5000 });
}
export function getCredentialDeleteButton() {
return cy.getByTestId('credential-delete-button');
}
export function getCredentialModalCloseButton() {
return getEditCredentialModal().find('.el-dialog__close').first();
}
/**
* Actions
*/
export function setCredentialConnectionParameterInputByName(name: string, value: string) {
getCredentialConnectionParameterInputByName(name).type(value);
}
export function saveCredential() {
getCredentialSaveButton().click({ force: true });
}
export function closeCredentialModal() {
getCredentialModalCloseButton().click();
}
export function setCredentialValues(values: Record<string, any>, save = true) {
Object.entries(values).forEach(([key, value]) => {
setCredentialConnectionParameterInputByName(key, value);
});
if (save) {
saveCredential();
closeCredentialModal();
}
}

View file

@ -0,0 +1,13 @@
/**
* Getters
*/
export const getWorkflowCredentialsModal = () => cy.getByTestId('setup-workflow-credentials-modal');
export const getContinueButton = () => cy.getByTestId('continue-button');
/**
* Actions
*/
export const closeModalFromContinueButton = () => getContinueButton().click();

View file

@ -0,0 +1,73 @@
/**
* Getters
*/
export function getCredentialSelect(eq = 0) {
return cy.getByTestId('node-credentials-select').eq(eq);
}
export function getCreateNewCredentialOption() {
return cy.getByTestId('node-credentials-select-item-new');
}
export function getBackToCanvasButton() {
return cy.getByTestId('back-to-canvas');
}
export function getExecuteNodeButton() {
return cy.getByTestId('node-execute-button');
}
export function getParameterInputByName(name: string) {
return cy.getByTestId(`parameter-input-${name}`);
}
export function getInputPanel() {
return cy.getByTestId('input-panel');
}
export function getMainPanel() {
return cy.getByTestId('node-parameters');
}
export function getOutputPanel() {
return cy.getByTestId('output-panel');
}
export function getOutputPanelDataContainer() {
return getOutputPanel().getByTestId('ndv-data-container');
}
export function getOutputPanelTable() {
return getOutputPanelDataContainer().get('table');
}
/**
* Actions
*/
export function openCredentialSelect(eq = 0) {
getCredentialSelect(eq).click();
}
export function setCredentialByName(name: string) {
openCredentialSelect();
getCredentialSelect().contains(name).click();
}
export function clickCreateNewCredential() {
openCredentialSelect();
getCreateNewCredentialOption().click();
}
export function clickGetBackToCanvas() {
getBackToCanvasButton().click();
}
export function clickExecuteNode() {
getExecuteNodeButton().click();
}
export function setParameterInputByName(name: string, value: string) {
getParameterInputByName(name).clear().type(value);
}

View file

@ -0,0 +1,14 @@
/**
* Getters
*/
export const getFormStep = () => cy.getByTestId('setup-credentials-form-step');
export const getStepHeading = ($el: JQuery<HTMLElement>) =>
cy.wrap($el).findChildByTestId('credential-step-heading');
export const getStepDescription = ($el: JQuery<HTMLElement>) =>
cy.wrap($el).findChildByTestId('credential-step-description');
export const getCreateAppCredentialsButton = (appName: string) =>
cy.get(`button:contains("Create new ${appName} credential")`);

View file

@ -0,0 +1,5 @@
/**
* Getters
*/
export const getSetupWorkflowCredentialsButton = () => cy.get(`button:contains("Set up template")`);

View file

@ -0,0 +1,32 @@
/**
* Getters
*/
export function getVersionUpdatesPanelOpenButton() {
return cy.getByTestId('version-updates-panel-button');
}
export function getVersionUpdatesPanel() {
return cy.getByTestId('version-updates-panel');
}
export function getVersionUpdatesPanelCloseButton() {
return getVersionUpdatesPanel().get('.el-drawer__close-btn').first();
}
export function getVersionCard() {
return cy.getByTestId('version-card');
}
/**
* Actions
*/
export function openVersionUpdatesPanel() {
getVersionUpdatesPanelOpenButton().click();
getVersionUpdatesPanel().should('be.visible');
}
export function closeVersionUpdatesPanel() {
getVersionUpdatesPanelCloseButton().click();
}

View file

@ -0,0 +1,142 @@
import { ROUTES } from '../constants';
import { getManualChatModal } from './modals/chat-modal';
/**
* Types
*/
export type EndpointType =
| 'ai_chain'
| 'ai_document'
| 'ai_embedding'
| 'ai_languageModel'
| 'ai_memory'
| 'ai_outputParser'
| 'ai_tool'
| 'ai_retriever'
| 'ai_textSplitter'
| 'ai_vectorRetriever'
| 'ai_vectorStore';
/**
* Getters
*/
export function getAddInputEndpointByType(nodeName: string, endpointType: EndpointType) {
return cy.get(
`.add-input-endpoint[data-jtk-scope-${endpointType}][data-endpoint-name="${nodeName}"]`,
);
}
export function getNodeCreatorItems() {
return cy.getByTestId('item-iterator-item');
}
export function getExecuteWorkflowButton() {
return cy.getByTestId('execute-workflow-button');
}
export function getManualChatButton() {
return cy.getByTestId('workflow-chat-button');
}
export function getNodes() {
return cy.getByTestId('canvas-node');
}
export function getNodeByName(name: string) {
return cy.getByTestId('canvas-node').filter(`[data-name="${name}"]`).eq(0);
}
export function getConnectionBySourceAndTarget(source: string, target: string) {
return cy
.get('.jtk-connector')
.filter(`[data-source-node="${source}"][data-target-node="${target}"]`)
.eq(0);
}
export function getNodeCreatorSearchBar() {
return cy.getByTestId('node-creator-search-bar');
}
export function getNodeCreatorPlusButton() {
return cy.getByTestId('node-creator-plus-button');
}
/**
* Actions
*/
export function addNodeToCanvas(
nodeDisplayName: string,
plusButtonClick = true,
preventNdvClose?: boolean,
action?: string,
) {
if (plusButtonClick) {
getNodeCreatorPlusButton().click();
}
getNodeCreatorSearchBar().type(nodeDisplayName);
getNodeCreatorSearchBar().type('{enter}');
cy.wait(500);
cy.get('body').then((body) => {
if (body.find('[data-test-id=node-creator]').length > 0) {
if (action) {
cy.contains(action).click();
} else {
// Select the first action
cy.get('[data-keyboard-nav-type="action"]').eq(0).click();
}
}
});
if (!preventNdvClose) cy.get('body').type('{esc}');
}
export function navigateToNewWorkflowPage(preventNodeViewUnload = true) {
cy.visit(ROUTES.NEW_WORKFLOW_PAGE);
cy.waitForLoad();
cy.window().then((win) => {
win.preventNodeViewBeforeUnload = preventNodeViewUnload;
});
}
export function addSupplementalNodeToParent(
nodeName: string,
endpointType: EndpointType,
parentNodeName: string,
) {
getAddInputEndpointByType(parentNodeName, endpointType).click({ force: true });
getNodeCreatorItems().contains(nodeName).click();
getConnectionBySourceAndTarget(parentNodeName, nodeName).should('exist');
}
export function addLanguageModelNodeToParent(nodeName: string, parentNodeName: string) {
addSupplementalNodeToParent(nodeName, 'ai_languageModel', parentNodeName);
}
export function addMemoryNodeToParent(nodeName: string, parentNodeName: string) {
addSupplementalNodeToParent(nodeName, 'ai_memory', parentNodeName);
}
export function addToolNodeToParent(nodeName: string, parentNodeName: string) {
addSupplementalNodeToParent(nodeName, 'ai_tool', parentNodeName);
}
export function addOutputParserNodeToParent(nodeName: string, parentNodeName: string) {
addSupplementalNodeToParent(nodeName, 'ai_outputParser', parentNodeName);
}
export function clickExecuteWorkflowButton() {
getExecuteWorkflowButton().click();
}
export function clickManualChatButton() {
getManualChatButton().click();
getManualChatModal().should('be.visible');
}
export function openNode(nodeName: string) {
getNodeByName(nodeName).dblclick();
}

View file

@ -12,6 +12,13 @@ export const INSTANCE_OWNER = {
lastName: randLastName(), lastName: randLastName(),
}; };
export const INSTANCE_ADMIN = {
email: 'admin@n8n.io',
password: DEFAULT_USER_PASSWORD,
firstName: randFirstName(),
lastName: randLastName(),
};
export const INSTANCE_MEMBERS = [ export const INSTANCE_MEMBERS = [
{ {
email: 'rebecca@n8n.io', email: 'rebecca@n8n.io',
@ -28,12 +35,13 @@ export const INSTANCE_MEMBERS = [
]; ];
export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger';
export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Execute Workflow"'; export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Test Workflow"';
export const MANUAL_CHAT_TRIGGER_NODE_NAME = 'Chat Trigger';
export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger'; export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
export const CODE_NODE_NAME = 'Code'; export const CODE_NODE_NAME = 'Code';
export const SET_NODE_NAME = 'Set'; export const SET_NODE_NAME = 'Set';
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields'; export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields';
export const IF_NODE_NAME = 'IF'; export const IF_NODE_NAME = 'If';
export const MERGE_NODE_NAME = 'Merge'; export const MERGE_NODE_NAME = 'Merge';
export const SWITCH_NODE_NAME = 'Switch'; export const SWITCH_NODE_NAME = 'Switch';
export const GMAIL_NODE_NAME = 'Gmail'; export const GMAIL_NODE_NAME = 'Gmail';
@ -41,6 +49,14 @@ export const TRELLO_NODE_NAME = 'Trello';
export const NOTION_NODE_NAME = 'Notion'; export const NOTION_NODE_NAME = 'Notion';
export const PIPEDRIVE_NODE_NAME = 'Pipedrive'; export const PIPEDRIVE_NODE_NAME = 'Pipedrive';
export const HTTP_REQUEST_NODE_NAME = 'HTTP Request'; export const HTTP_REQUEST_NODE_NAME = 'HTTP Request';
export const AGENT_NODE_NAME = 'AI Agent';
export const BASIC_LLM_CHAIN_NODE_NAME = 'Basic LLM Chain';
export const AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME = 'Window Buffer Memory';
export const AI_TOOL_CALCULATOR_NODE_NAME = 'Calculator';
export const AI_TOOL_CODE_NODE_NAME = 'Custom Code Tool';
export const AI_TOOL_WIKIPEDIA_NODE_NAME = 'Wikipedia';
export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model';
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
export const META_KEY = Cypress.platform === 'darwin' ? '{meta}' : '{ctrl}'; export const META_KEY = Cypress.platform === 'darwin' ? '{meta}' : '{ctrl}';
@ -48,3 +64,7 @@ export const NEW_GOOGLE_ACCOUNT_NAME = 'Gmail account';
export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account'; export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account';
export const NEW_NOTION_ACCOUNT_NAME = 'Notion account'; export const NEW_NOTION_ACCOUNT_NAME = 'Notion account';
export const NEW_QUERY_AUTH_ACCOUNT_NAME = 'Query Auth account'; export const NEW_QUERY_AUTH_ACCOUNT_NAME = 'Query Auth account';
export const ROUTES = {
NEW_WORKFLOW_PAGE: '/workflow/new',
};

View file

@ -1,12 +1,14 @@
import { CODE_NODE_NAME, SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from './../constants'; import { CODE_NODE_NAME, SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME } from './../constants';
import { SCHEDULE_TRIGGER_NODE_NAME } from '../constants'; import { SCHEDULE_TRIGGER_NODE_NAME } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { MessageBox as MessageBoxClass } from '../pages/modals/message-box';
import { NDV } from '../pages/ndv'; import { NDV } from '../pages/ndv';
// Suite-specific constants // Suite-specific constants
const CODE_NODE_NEW_NAME = 'Something else'; const CODE_NODE_NEW_NAME = 'Something else';
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
const messageBox = new MessageBoxClass();
const ndv = new NDV(); const ndv = new NDV();
describe('Undo/Redo', () => { describe('Undo/Redo', () => {
@ -44,7 +46,7 @@ describe('Undo/Redo', () => {
WorkflowPage.getters WorkflowPage.getters
.canvasNodeByName('Code') .canvasNodeByName('Code')
.should('have.css', 'left', '860px') .should('have.css', 'left', '860px')
.should('have.css', 'top', '220px') .should('have.css', 'top', '220px');
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 2); WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
@ -62,16 +64,13 @@ describe('Undo/Redo', () => {
WorkflowPage.getters WorkflowPage.getters
.canvasNodeByName('Code') .canvasNodeByName('Code')
.should('have.css', 'left', '860px') .should('have.css', 'left', '860px')
.should('have.css', 'top', '220px') .should('have.css', 'top', '220px');
}); });
it('should undo/redo deleting node using delete button', () => { it('should undo/redo deleting node using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME);
.canvasNodeByName(CODE_NODE_NAME)
.find('[data-test-id=delete-node-button]')
.click({ force: true });
WorkflowPage.getters.canvasNodes().should('have.have.length', 1); WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
@ -137,21 +136,21 @@ describe('Undo/Redo', () => {
WorkflowPage.getters WorkflowPage.getters
.canvasNodeByName('Code') .canvasNodeByName('Code')
.should('have.css', 'left', '740px') .should('have.css', 'left', '740px')
.should('have.css', 'top', '320px') .should('have.css', 'top', '320px');
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters WorkflowPage.getters
.canvasNodeByName('Code') .canvasNodeByName('Code')
.should('have.css', 'left', '640px') .should('have.css', 'left', '640px')
.should('have.css', 'top', '220px') .should('have.css', 'top', '220px');
WorkflowPage.actions.hitRedo(); WorkflowPage.actions.hitRedo();
WorkflowPage.getters WorkflowPage.getters
.canvasNodeByName('Code') .canvasNodeByName('Code')
.should('have.css', 'left', '740px') .should('have.css', 'left', '740px')
.should('have.css', 'top', '320px') .should('have.css', 'top', '320px');
}); });
it('should undo/redo deleting a connection by pressing delete button', () => { it('should undo/redo deleting a connection using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.nodeConnections().realHover(); WorkflowPage.getters.nodeConnections().realHover();
@ -177,14 +176,10 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
}); });
it('should undo/redo disabling a node using disable button', () => { it('should undo/redo disabling a node using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters WorkflowPage.actions.disableNode(CODE_NODE_NAME);
.canvasNodes()
.last()
.find('[data-test-id="disable-node-button"]')
.click({ force: true });
WorkflowPage.getters.disabledNodes().should('have.length', 1); WorkflowPage.getters.disabledNodes().should('have.length', 1);
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters.disabledNodes().should('have.length', 0); WorkflowPage.getters.disabledNodes().should('have.length', 0);
@ -252,11 +247,7 @@ describe('Undo/Redo', () => {
it('should undo/redo duplicating a node', () => { it('should undo/redo duplicating a node', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters WorkflowPage.actions.duplicateNode(CODE_NODE_NAME);
.canvasNodes()
.last()
.find('[data-test-id="duplicate-node-button"]')
.click({ force: true });
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.actions.hitRedo(); WorkflowPage.actions.hitRedo();
@ -276,9 +267,6 @@ describe('Undo/Redo', () => {
}); });
it('should undo/redo multiple steps', () => { it('should undo/redo multiple steps', () => {
const initialPosition = {left: '420px', top: '220px'};
const movedPosition = {left: '540px', top: '360px'};
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
// WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME); // WorkflowPage.actions.addNodeToCanvas(SET_NODE_NAME);
@ -289,18 +277,21 @@ describe('Undo/Redo', () => {
// Disable last node // Disable last node
WorkflowPage.getters.canvasNodes().last().click(); WorkflowPage.getters.canvasNodes().last().click();
WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.actions.hitDisableNodeShortcut();
// Move first one
WorkflowPage.getters.canvasNodes()
.first()
.should('have.css', 'left', initialPosition.left)
.should('have.css', 'top', initialPosition.top)
// Move first one
WorkflowPage.actions
.getNodePosition(WorkflowPage.getters.canvasNodes().first())
.then((initialPosition) => {
WorkflowPage.getters.canvasNodes().first().click(); WorkflowPage.getters.canvasNodes().first().click();
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true }); cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], {
WorkflowPage.getters.canvasNodes() clickToFinish: true,
});
WorkflowPage.getters
.canvasNodes()
.first() .first()
.should('have.css', 'left', movedPosition.left) .should('have.css', 'left', `${initialPosition.left + 120}px`)
.should('have.css', 'top', movedPosition.top) .should('have.css', 'top', `${initialPosition.top + 140}px`);
// Delete the set node // Delete the set node
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click().click(); WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click().click();
cy.get('body').type('{backspace}'); cy.get('body').type('{backspace}');
@ -311,10 +302,11 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 3); WorkflowPage.getters.nodeConnections().should('have.length', 3);
// Second undo: Should move first node to it's original position // Second undo: Should move first node to it's original position
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes() WorkflowPage.getters
.canvasNodes()
.first() .first()
.should('have.css', 'left', initialPosition.left) .should('have.css', 'left', `${initialPosition.left}px`)
.should('have.css', 'top', initialPosition.top) .should('have.css', 'top', `${initialPosition.top}px`);
// Third undo: Should enable last node // Third undo: Should enable last node
WorkflowPage.actions.hitUndo(); WorkflowPage.actions.hitUndo();
WorkflowPage.getters.disabledNodes().should('have.length', 0); WorkflowPage.getters.disabledNodes().should('have.length', 0);
@ -324,13 +316,76 @@ describe('Undo/Redo', () => {
WorkflowPage.getters.disabledNodes().should('have.length', 1); WorkflowPage.getters.disabledNodes().should('have.length', 1);
// Second redo: Should move the first node // Second redo: Should move the first node
WorkflowPage.actions.hitRedo(); WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes() WorkflowPage.getters
.canvasNodes()
.first() .first()
.should('have.css', 'left', movedPosition.left) .should('have.css', 'left', `${initialPosition.left + 120}px`)
.should('have.css', 'top', movedPosition.top) .should('have.css', 'top', `${initialPosition.top + 140}px`);
// Third redo: Should delete the Set node // Third redo: Should delete the Set node
WorkflowPage.actions.hitRedo(); WorkflowPage.actions.hitRedo();
WorkflowPage.getters.canvasNodes().should('have.length', 3); WorkflowPage.getters.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 2); WorkflowPage.getters.nodeConnections().should('have.length', 2);
}); });
});
it('should be able to copy and paste pinned data nodes in workflows with dynamic Switch node', () => {
cy.fixture('Test_workflow_form_switch.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
WorkflowPage.actions.zoomToFit();
WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch'))
.should('have.css', 'left', `637px`)
.should('have.css', 'top', `501px`);
cy.fixture('Test_workflow_form_switch.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
WorkflowPage.getters.canvasNodes().should('have.length', 4);
WorkflowPage.getters.nodeConnections().should('have.length', 2);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch')).should('have.length', 1);
cy.get(WorkflowPage.getters.getEndpointSelector('input', 'Switch'))
.should('have.css', 'left', `637px`)
.should('have.css', 'top', `501px`);
});
it('should not undo/redo when NDV or a modal is open', () => {
WorkflowPage.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, { keepNdvOpen: true });
// Try while NDV is open
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
ndv.getters.backToCanvas().click();
// Try while modal is open
cy.getByTestId('menu-item').contains('About n8n').click({ force: true });
cy.getByTestId('about-modal').should('be.visible');
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
cy.getByTestId('close-about-modal-button').click();
// Should work now
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 0);
});
it('should not undo/redo when NDV or a prompt is open', () => {
WorkflowPage.actions.addInitialNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME, { keepNdvOpen: false });
WorkflowPage.getters.workflowMenu().click();
WorkflowPage.getters.workflowMenuItemImportFromURLItem().should('be.visible');
WorkflowPage.getters.workflowMenuItemImportFromURLItem().click();
// Try while prompt is open
messageBox.getters.header().click();
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 1);
// Close prompt and try again
messageBox.actions.cancel();
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 0);
});
}); });

View file

@ -67,6 +67,6 @@ describe('Inline expression editor', () => {
WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('{{');
// Resolving $parameter is slow, especially on CI runner // Resolving $parameter is slow, especially on CI runner
WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]'); WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]');
WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^get$/); WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'getAll');
}); });
}); });

View file

@ -134,7 +134,7 @@ describe('Canvas Actions', () => {
.canvasNodes() .canvasNodes()
.last() .last()
.should('have.css', 'left', '860px') .should('have.css', 'left', '860px')
.should('have.css', 'top', '220px') .should('have.css', 'top', '220px');
}); });
it('should delete connections by pressing the delete button', () => { it('should delete connections by pressing the delete button', () => {
@ -163,21 +163,29 @@ describe('Canvas Actions', () => {
.find('[data-test-id="execute-node-button"]') .find('[data-test-id="execute-node-button"]')
.click({ force: true }); .click({ force: true });
WorkflowPage.getters.successToast().should('contain', 'Node executed successfully'); WorkflowPage.getters.successToast().should('contain', 'Node executed successfully');
WorkflowPage.actions.executeNode(CODE_NODE_NAME);
WorkflowPage.getters.successToast().should('contain', 'Node executed successfully');
}); });
it('should copy selected nodes', () => { it('should copy selected nodes', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.selectAll(); WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitCopy(); WorkflowPage.actions.hitCopy();
WorkflowPage.getters.successToast().should('contain', 'Copied!'); WorkflowPage.getters.successToast().should('contain', 'Copied!');
WorkflowPage.actions.copyNode(CODE_NODE_NAME);
WorkflowPage.getters.successToast().should('contain', 'Copied!');
}); });
it('should select all nodes', () => { it('should select/deselect all nodes', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.selectAll(); WorkflowPage.actions.selectAll();
WorkflowPage.getters.selectedNodes().should('have.length', 2); WorkflowPage.getters.selectedNodes().should('have.length', 2);
WorkflowPage.actions.deselectAll();
WorkflowPage.getters.selectedNodes().should('have.length', 0);
}); });
it('should select nodes using arrow keys', () => { it('should select nodes using arrow keys', () => {
@ -205,22 +213,21 @@ describe('Canvas Actions', () => {
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.last() .last()
.findChildByTestId('disable-node-button').as('disableNodeButton'); .findChildByTestId('execute-node-button')
cy.drag('@disableNodeButton', [200, 200]); .as('executeNodeButton');
cy.drag('@executeNodeButton', [200, 200]);
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]); WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
}); });
it('should not break lasso selection with multiple clicks on node action buttons', () => { it('should not break lasso selection with multiple clicks on node action buttons', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]); WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
WorkflowPage.getters WorkflowPage.getters.canvasNodes().last().as('lastNode');
.canvasNodes() cy.get('@lastNode').findChildByTestId('execute-node-button').as('executeNodeButton');
.last().as('lastNode');
cy.get('@lastNode').findChildByTestId('disable-node-button').as('disableNodeButton');
for (let i = 0; i < 20; i++) { for (let i = 0; i < 20; i++) {
cy.get('@lastNode').realHover(); cy.get('@lastNode').realHover();
cy.get('@disableNodeButton').should('be.visible'); cy.get('@executeNodeButton').should('be.visible');
cy.get('@disableNodeButton').realTouch(); cy.get('@executeNodeButton').realTouch();
cy.getByTestId('execute-workflow-button').realHover(); cy.getByTestId('execute-workflow-button').realHover();
WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]); WorkflowPage.actions.testLassoSelection([100, 100], [200, 200]);
} }

View file

@ -16,12 +16,13 @@ const NDVDialog = new NDV();
const DEFAULT_ZOOM_FACTOR = 1; const DEFAULT_ZOOM_FACTOR = 1;
const ZOOM_IN_X1_FACTOR = 1.25; // Zoom in factor after one click const ZOOM_IN_X1_FACTOR = 1.25; // Zoom in factor after one click
const ZOOM_IN_X2_FACTOR = 1.5625; // Zoom in factor after two clicks const ZOOM_IN_X2_FACTOR = 1.5625; // Zoom in factor after two clicks
const ZOOM_OUT_X1_FACTOR = 0.75; const ZOOM_OUT_X1_FACTOR = 0.8;
const ZOOM_OUT_X2_FACTOR = 0.5625; const ZOOM_OUT_X2_FACTOR = 0.64;
const PINCH_ZOOM_IN_FACTOR = 1.32; const PINCH_ZOOM_IN_FACTOR = 1.05702;
const PINCH_ZOOM_OUT_FACTOR = 0.4752; const PINCH_ZOOM_OUT_FACTOR = 0.946058;
const RENAME_NODE_NAME = 'Something else'; const RENAME_NODE_NAME = 'Something else';
const RENAME_NODE_NAME2 = 'Something different';
describe('Canvas Node Manipulation and Navigation', () => { describe('Canvas Node Manipulation and Navigation', () => {
beforeEach(() => { beforeEach(() => {
@ -30,22 +31,30 @@ describe('Canvas Node Manipulation and Navigation', () => {
it('should add switch node and test connections', () => { it('should add switch node and test connections', () => {
const desiredOutputs = 4; const desiredOutputs = 4;
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
WorkflowPage.actions.addNodeToCanvas(SWITCH_NODE_NAME, true, true); WorkflowPage.actions.addNodeToCanvas(SWITCH_NODE_NAME, true, true);
for (let i = 0; i < desiredOutputs; i++) { for (let i = 0; i < desiredOutputs; i++) {
cy.contains('Add Routing Rule').click() cy.contains('Add Routing Rule').click();
} }
NDVDialog.actions.close() NDVDialog.actions.close();
for (let i = 0; i < desiredOutputs; i++) { for (let i = 0; i < desiredOutputs; i++) {
WorkflowPage.getters.canvasNodePlusEndpointByName(SWITCH_NODE_NAME, i).click({ force: true }); WorkflowPage.getters.canvasNodePlusEndpointByName(SWITCH_NODE_NAME, i).click({ force: true });
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false); WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
} }
WorkflowPage.getters.nodeViewBackground().click({ force: true });
WorkflowPage.getters.canvasNodePlusEndpointByName(`${EDIT_FIELDS_SET_NODE_NAME}3`).click();
WorkflowPage.actions.addNodeToCanvas(SWITCH_NODE_NAME, false);
WorkflowPage.actions.saveWorkflowOnButtonClick(); WorkflowPage.actions.saveWorkflowOnButtonClick();
cy.reload(); cy.reload();
cy.waitForLoad(); cy.waitForLoad();
// Make sure outputless switch was connected correctly
cy.get(
`[data-target-node="${SWITCH_NODE_NAME}1"][data-source-node="${EDIT_FIELDS_SET_NODE_NAME}3"]`,
).should('be.visible');
// Make sure all connections are there after reload // Make sure all connections are there after reload
for (let i = 0; i < desiredOutputs; i++) { for (let i = 0; i < desiredOutputs; i++) {
const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`; const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`;
@ -121,13 +130,10 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.get('.jtk-connector').should('have.length', 4); cy.get('.jtk-connector').should('have.length', 4);
}); });
it('should delete node using node action button', () => { it('should delete node using context menu', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters WorkflowPage.actions.deleteNodeFromContextMenu(CODE_NODE_NAME);
.canvasNodeByName(CODE_NODE_NAME)
.find('[data-test-id=delete-node-button]')
.click({ force: true });
WorkflowPage.getters.canvasNodes().should('have.length', 1); WorkflowPage.getters.canvasNodes().should('have.length', 1);
WorkflowPage.getters.nodeConnections().should('have.length', 0); WorkflowPage.getters.nodeConnections().should('have.length', 0);
}); });
@ -154,13 +160,38 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
}); });
it('should delete multiple nodes', () => { it('should delete multiple nodes (context menu or shortcut)', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500); cy.wait(500);
WorkflowPage.actions.selectAll(); WorkflowPage.actions.selectAll();
cy.get('body').type('{backspace}'); cy.get('body').type('{backspace}');
WorkflowPage.getters.canvasNodes().should('have.length', 0); WorkflowPage.getters.canvasNodes().should('have.length', 0);
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500);
WorkflowPage.actions.selectAllFromContextMenu();
WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('delete');
WorkflowPage.getters.canvasNodes().should('have.length', 0);
});
it('should delete multiple nodes (context menu or shortcut)', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500);
WorkflowPage.actions.selectAll();
cy.get('body').type('{backspace}');
WorkflowPage.getters.canvasNodes().should('have.length', 0);
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500);
WorkflowPage.actions.selectAllFromContextMenu();
WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('delete');
WorkflowPage.getters.canvasNodes().should('have.length', 0);
}); });
it('should move node', () => { it('should move node', () => {
@ -168,12 +199,13 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true }); cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
WorkflowPage.getters WorkflowPage.getters
.canvasNodes() .canvasNodes()
.last() .last()
.should('have.css', 'left', '740px') .should('have.css', 'left', '740px')
.should('have.css', 'top', '320px') .should('have.css', 'top', '320px');
}); });
it('should zoom in', () => { it('should zoom in', () => {
@ -214,8 +246,8 @@ describe('Canvas Node Manipulation and Navigation', () => {
); );
}); });
it('should zoom using pinch to zoom', () => { it('should zoom using scroll or pinch gesture', () => {
WorkflowPage.actions.pinchToZoom(2, 'zoomIn'); WorkflowPage.actions.pinchToZoom(1, 'zoomIn');
WorkflowPage.getters WorkflowPage.getters
.nodeView() .nodeView()
.should( .should(
@ -224,7 +256,11 @@ describe('Canvas Node Manipulation and Navigation', () => {
`matrix(${PINCH_ZOOM_IN_FACTOR}, 0, 0, ${PINCH_ZOOM_IN_FACTOR}, 0, 0)`, `matrix(${PINCH_ZOOM_IN_FACTOR}, 0, 0, ${PINCH_ZOOM_IN_FACTOR}, 0, 0)`,
); );
WorkflowPage.actions.pinchToZoom(4, 'zoomOut'); WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
// Zoom in 1x + Zoom out 1x should reset to default (=1)
WorkflowPage.getters.nodeView().should('have.css', 'transform', `matrix(1, 0, 0, 1, 0, 0)`);
WorkflowPage.actions.pinchToZoom(1, 'zoomOut');
WorkflowPage.getters WorkflowPage.getters
.nodeView() .nodeView()
.should( .should(
@ -259,39 +295,42 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodes().last().should('be.visible'); WorkflowPage.getters.canvasNodes().last().should('be.visible');
}); });
it('should disable node by pressing the disable button', () => { it('should disable node (context menu or shortcut)', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters
.canvasNodes()
.last()
.find('[data-test-id="disable-node-button"]')
.click({ force: true });
WorkflowPage.getters.disabledNodes().should('have.length', 1);
});
it('should disable node using keyboard shortcut', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodes().last().click(); WorkflowPage.getters.canvasNodes().last().click();
WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 1); WorkflowPage.getters.disabledNodes().should('have.length', 1);
WorkflowPage.actions.disableNode(CODE_NODE_NAME);
WorkflowPage.getters.disabledNodes().should('have.length', 0);
}); });
it('should disable multiple nodes', () => { it('should disable multiple nodes (context menu or shortcut)', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.get('body').type('{esc}'); cy.get('body').type('{esc}');
cy.get('body').type('{esc}'); cy.get('body').type('{esc}');
WorkflowPage.actions.selectAll(); WorkflowPage.actions.selectAll();
// Keyboard shortcut
WorkflowPage.actions.hitDisableNodeShortcut(); WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 2); WorkflowPage.getters.disabledNodes().should('have.length', 2);
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 0);
// Context menu
WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('toggle_activation');
WorkflowPage.getters.disabledNodes().should('have.length', 2);
WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('toggle_activation');
WorkflowPage.getters.disabledNodes().should('have.length', 0);
}); });
it('should rename node using keyboard shortcut', () => { it('should rename node (context menu or shortcut)', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters.canvasNodes().last().click(); WorkflowPage.getters.canvasNodes().last().click();
@ -300,19 +339,25 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.get('body').type(RENAME_NODE_NAME); cy.get('body').type(RENAME_NODE_NAME);
cy.get('body').type('{enter}'); cy.get('body').type('{enter}');
WorkflowPage.getters.canvasNodeByName(RENAME_NODE_NAME).should('exist'); WorkflowPage.getters.canvasNodeByName(RENAME_NODE_NAME).should('exist');
WorkflowPage.actions.renameNode(RENAME_NODE_NAME);
cy.get('.rename-prompt').should('be.visible');
cy.get('body').type(RENAME_NODE_NAME2);
cy.get('body').type('{enter}');
WorkflowPage.getters.canvasNodeByName(RENAME_NODE_NAME2).should('exist');
}); });
it('should duplicate node', () => { it('should duplicate nodes (context menu or shortcut)', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click(); WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.getters WorkflowPage.actions.duplicateNode(CODE_NODE_NAME);
.canvasNodes()
.last()
.find('[data-test-id="duplicate-node-button"]')
.click({ force: true });
WorkflowPage.getters.canvasNodes().should('have.length', 3); WorkflowPage.getters.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 1); WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitDuplicateNodeShortcut();
WorkflowPage.getters.canvasNodes().should('have.length', 5);
}); });
// ADO-1240: Connections would get deleted after activating and deactivating NodeView // ADO-1240: Connections would get deleted after activating and deactivating NodeView
@ -344,5 +389,47 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.waitForLoad(); cy.waitForLoad();
WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.canvasNodes().should('have.length', 2);
cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 1); cy.get('.rect-input-endpoint.jtk-endpoint-connected').should('have.length', 1);
}) });
it('should remove unknown credentials on pasting workflow', () => {
cy.fixture('workflow-with-unknown-credentials.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
WorkflowPage.actions.openNodeFromContextMenu('n8n');
cy.get('[class*=hasIssues]').should('have.length', 1);
NDVDialog.actions.close();
});
});
it('should render connections correctly if unkown nodes are present', () => {
const unknownNodeName = 'Unknown node';
cy.createFixtureWorkflow('workflow-with-unknown-nodes.json', 'Unknown nodes');
WorkflowPage.getters.canvasNodeByName(`${unknownNodeName} 1`).should('exist');
WorkflowPage.getters.canvasNodeByName(`${unknownNodeName} 2`).should('exist');
WorkflowPage.actions.zoomToFit();
cy.draganddrop(
WorkflowPage.getters.getEndpointSelector('plus', `${unknownNodeName} 1`),
WorkflowPage.getters.getEndpointSelector('input', EDIT_FIELDS_SET_NODE_NAME),
);
cy.draganddrop(
WorkflowPage.getters.getEndpointSelector('plus', `${unknownNodeName} 2`),
WorkflowPage.getters.getEndpointSelector('input', `${EDIT_FIELDS_SET_NODE_NAME}1`),
);
WorkflowPage.actions.executeWorkflow();
cy.contains('Unrecognized node type').should('be.visible');
WorkflowPage.actions.deselectAll();
WorkflowPage.actions.deleteNodeFromContextMenu(`${unknownNodeName} 1`);
WorkflowPage.actions.deleteNodeFromContextMenu(`${unknownNodeName} 2`);
WorkflowPage.actions.executeWorkflow();
cy.contains('Unrecognized node type').should('not.exist');
});
}); });

View file

@ -3,6 +3,7 @@ import {
MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_NAME,
PIPEDRIVE_NODE_NAME, PIPEDRIVE_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME,
BACKEND_BASE_URL,
} from '../constants'; } from '../constants';
import { WorkflowPage, NDV } from '../pages'; import { WorkflowPage, NDV } from '../pages';
@ -62,13 +63,77 @@ describe('Data pinning', () => {
workflowPage.actions.saveWorkflowOnButtonClick(); workflowPage.actions.saveWorkflowOnButtonClick();
cy.reload();
workflowPage.actions.openNode('Schedule Trigger'); workflowPage.actions.openNode('Schedule Trigger');
ndv.getters.outputTableHeaders().first().should('include.text', 'test'); ndv.getters.outputTableHeaders().first().should('include.text', 'test');
ndv.getters.outputTbodyCell(1, 0).should('include.text', 1); ndv.getters.outputTbodyCell(1, 0).should('include.text', 1);
}); });
it('Should be duplicating pin data when duplicating node', () => {
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
ndv.getters.container().should('be.visible');
ndv.getters.pinDataButton().should('not.exist');
ndv.getters.editPinnedDataButton().should('be.visible');
ndv.actions.setPinnedData([{ test: 1 }]);
ndv.actions.close();
workflowPage.actions.duplicateNode(EDIT_FIELDS_SET_NODE_NAME);
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.openNode('Edit Fields1');
ndv.getters.outputTableHeaders().first().should('include.text', 'test');
ndv.getters.outputTbodyCell(1, 0).should('include.text', 1);
});
it('Should be able to pin data from canvas (context menu or shortcut)', () => {
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
workflowPage.actions.openContextMenu(EDIT_FIELDS_SET_NODE_NAME, 'overflow-button');
workflowPage.getters
.contextMenuAction('toggle_pin')
.parent()
.should('have.class', 'is-disabled');
// Unpin using context menu
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
ndv.actions.setPinnedData([{ test: 1 }]);
ndv.actions.close();
workflowPage.actions.pinNode(EDIT_FIELDS_SET_NODE_NAME);
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
ndv.getters.nodeOutputHint().should('exist');
ndv.actions.close();
// Unpin using shortcut
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
ndv.actions.setPinnedData([{ test: 1 }]);
ndv.actions.close();
workflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click();
workflowPage.actions.hitPinNodeShortcut();
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
ndv.getters.nodeOutputHint().should('exist');
});
it('Should show an error when maximum pin data size is exceeded', () => {
workflowPage.actions.addInitialNodeToCanvas('Schedule Trigger');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
ndv.getters.container().should('be.visible');
ndv.getters.pinDataButton().should('not.exist');
ndv.getters.editPinnedDataButton().should('be.visible');
ndv.actions.setPinnedData([
{
test: '1'.repeat(Cypress.env('MAX_PINNED_DATA_SIZE')),
},
]);
workflowPage.getters
.errorToast()
.should('contain', 'Workflow has reached the maximum allowed pinned data size');
});
it('Should be able to reference paired items in a node located before pinned data', () => { it('Should be able to reference paired items in a node located before pinned data', () => {
workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true); workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true);
@ -86,10 +151,25 @@ describe('Data pinning', () => {
cy.get('div').contains(output).should('be.visible'); cy.get('div').contains(output).should('be.visible');
}); });
it('should use pin data in manual executions that are started by a webhook', () => {
cy.createFixtureWorkflow('Test_workflow_webhook_with_pin_data.json', 'Test');
workflowPage.actions.executeWorkflow();
cy.request('GET', `${BACKEND_BASE_URL}/webhook-test/b0d79ddb-df2d-49b1-8555-9fa2b482608f`).then((response) => {
expect(response.status).to.eq(200);
});
workflowPage.actions.openNode('End');
ndv.getters.outputTableRow(1).should('exist')
ndv.getters.outputTableRow(1).should('have.text', 'pin-overwritten');
});
}); });
function setExpressionOnStringValueInSet(expression: string) { function setExpressionOnStringValueInSet(expression: string) {
cy.get('button').contains('Execute node').click(); cy.get('button').contains('Test step').click();
cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click(); cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click();
ndv.getters.nthParam(4).contains('Expression').invoke('show').click(); ndv.getters.nthParam(4).contains('Expression').invoke('show').click();

View file

@ -80,14 +80,14 @@ describe('Data mapping', () => {
.parameterExpressionPreview('value') .parameterExpressionPreview('value')
.should('include.text', '0') .should('include.text', '0')
.invoke('css', 'color') .invoke('css', 'color')
.should('equal', 'rgb(125, 125, 135)'); .should('equal', 'rgb(113, 116, 122)');
ndv.getters.inputTbodyCell(2, 0).realHover(); ndv.getters.inputTbodyCell(2, 0).realHover();
ndv.getters ndv.getters
.parameterExpressionPreview('value') .parameterExpressionPreview('value')
.should('include.text', '1') .should('include.text', '1')
.invoke('css', 'color') .invoke('css', 'color')
.should('equal', 'rgb(125, 125, 135)'); .should('equal', 'rgb(113, 116, 122)');
ndv.actions.execute(); ndv.actions.execute();
@ -96,14 +96,14 @@ describe('Data mapping', () => {
.parameterExpressionPreview('value') .parameterExpressionPreview('value')
.should('include.text', '0') .should('include.text', '0')
.invoke('css', 'color') .invoke('css', 'color')
.should('equal', 'rgb(125, 125, 135)'); // todo update color .should('equal', 'rgb(113, 116, 122)'); // todo update color
ndv.getters.outputTbodyCell(2, 0).realHover(); ndv.getters.outputTbodyCell(2, 0).realHover();
ndv.getters ndv.getters
.parameterExpressionPreview('value') .parameterExpressionPreview('value')
.should('include.text', '1') .should('include.text', '1')
.invoke('css', 'color') .invoke('css', 'color')
.should('equal', 'rgb(125, 125, 135)'); .should('equal', 'rgb(113, 116, 122)');
}); });
it('maps expressions from json view', () => { it('maps expressions from json view', () => {
@ -235,11 +235,8 @@ describe('Data mapping', () => {
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.addNodeToCanvas('Item Lists'); workflowPage.actions.addNodeToCanvas('Sort');
workflowPage.actions.openNode('Item Lists'); workflowPage.actions.openNode('Sort');
ndv.getters.parameterInput('operation').click();
getVisibleSelect().find('li').contains('Sort').click();
ndv.getters.nodeParameters().find('button').contains('Add Field To Sort By').click(); ndv.getters.nodeParameters().find('button').contains('Add Field To Sort By').click();

View file

@ -1,7 +1,5 @@
import { WorkflowPage, NDV } from '../pages'; import { WorkflowPage, NDV } from '../pages';
import { v4 as uuid } from 'uuid'; import { getVisibleSelect } from '../utils';
import { getPopper, getVisiblePopper, getVisibleSelect } from '../utils';
import { META_KEY } from '../constants';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -13,10 +11,7 @@ describe('n8n Form Trigger', () => {
it("add node by clicking on 'On form submission'", () => { it("add node by clicking on 'On form submission'", () => {
workflowPage.getters.canvasPlusButton().click(); workflowPage.getters.canvasPlusButton().click();
cy.get('#node-view-root > div:nth-child(2) > div > div > aside ') workflowPage.getters.nodeCreatorNodeItems().contains('On form submission').click();
.find('span')
.contains('On form submission')
.click();
ndv.getters.parameterInput('formTitle').type('Test Form'); ndv.getters.parameterInput('formTitle').type('Test Form');
ndv.getters.parameterInput('formDescription').type('Test Form Description'); ndv.getters.parameterInput('formDescription').type('Test Form Description');
ndv.getters.parameterInput('fieldLabel').type('Test Field 1'); ndv.getters.parameterInput('fieldLabel').type('Test Field 1');
@ -76,12 +71,25 @@ describe('n8n Form Trigger', () => {
) )
.find('input') .find('input')
.type('Option 2'); .type('Option 2');
//add optionall submitted message
cy.get('.param-options > .button').click(); //add optional submitted message
cy.get('.indent > .parameter-item') cy.get('.param-options').click();
.find('input') cy.contains('span', 'Text to Show')
.should('exist')
.parent()
.parent()
.next()
.children()
.children()
.children()
.children()
.children()
.children()
.children()
.first()
.clear() .clear()
.type('Your test form was successfully submitted'); .type('Your test form was successfully submitted');
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist'); workflowPage.getters.nodeIssuesByName('n8n Form Trigger').should('not.exist');
}); });

View file

@ -187,12 +187,14 @@ describe('Webhook Trigger node', async () => {
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.actions.addNodeToCanvas('Convert to/from binary data'); workflowPage.actions.addNodeToCanvas('Convert to File');
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.actions.openNode('Convert to/from binary data'); workflowPage.actions.openNode('Convert to File');
cy.getByTestId('parameter-input-operation').click();
getVisibleSelect().find('.option-headline').contains('Convert to JSON').click();
cy.getByTestId('parameter-input-mode').click(); cy.getByTestId('parameter-input-mode').click();
getVisibleSelect().find('.option-headline').contains('JSON to Binary').click(); getVisibleSelect().find('.option-headline').contains('Each Item to Separate File').click();
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();

View file

@ -1,4 +1,4 @@
import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants'; import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN } from '../constants';
import { import {
CredentialsModal, CredentialsModal,
CredentialsPage, CredentialsPage,
@ -7,6 +7,7 @@ import {
WorkflowSharingModal, WorkflowSharingModal,
WorkflowsPage, WorkflowsPage,
} from '../pages'; } from '../pages';
import { getVisibleSelect } from '../utils';
/** /**
* User U1 - Instance owner * User U1 - Instance owner
@ -59,6 +60,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
cy.visit(workflowsPage.url); cy.visit(workflowsPage.url);
workflowsPage.getters.createWorkflowButton().click(); workflowsPage.getters.createWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_1.json', 'Workflow W2'); cy.createFixtureWorkflow('Test_workflow_1.json', 'Workflow W2');
workflowPage.actions.saveWorkflowOnButtonClick();
cy.url().then((url) => { cy.url().then((url) => {
workflowW2Url = url; workflowW2Url = url;
}); });
@ -96,6 +98,26 @@ describe('Sharing', { disableAutoLogin: true }, () => {
ndv.actions.close(); ndv.actions.close();
}); });
it('should open W1, add node using C2 as U2', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCard('Workflow W1').click();
workflowPage.actions.addNodeToCanvas('Airtable', true, true);
ndv.getters.credentialInput().find('input').should('have.value', 'Credential C2');
ndv.actions.close();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.openNode('Notion');
ndv.getters
.credentialInput()
.find('input')
.should('have.value', 'Credential C1')
.should('be.enabled');
ndv.actions.close();
});
it('should not have access to W2, as U3', () => { it('should not have access to W2, as U3', () => {
cy.signin(INSTANCE_MEMBERS[1]); cy.signin(INSTANCE_MEMBERS[1]);
@ -128,4 +150,41 @@ describe('Sharing', { disableAutoLogin: true }, () => {
credentialsPage.getters.credentialCard('Credential C2').click(); credentialsPage.getters.credentialCard('Credential C2').click();
credentialsModal.getters.testSuccessTag().should('be.visible'); credentialsModal.getters.testSuccessTag().should('be.visible');
}); });
it('should work for admin role on credentials created by others (also can share it with themselves)', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.visit(credentialsPage.url);
credentialsPage.getters.createCredentialButton().click();
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
credentialsModal.getters.newCredentialTypeButton().click({ force: true });
credentialsModal.getters.connectionParameter('Internal Integration Secret').type('1234567890');
credentialsModal.actions.setName('Credential C3');
credentialsModal.actions.save();
credentialsModal.actions.close();
cy.signout();
cy.signin(INSTANCE_ADMIN);
cy.visit(credentialsPage.url);
credentialsPage.getters.credentialCard('Credential C3').click();
credentialsModal.getters.testSuccessTag().should('be.visible');
cy.get('input').should('not.have.length');
credentialsModal.actions.changeTab('Sharing');
cy.contains(
'You can view this credential because you have permission to read and share',
).should('be.visible');
credentialsModal.getters.usersSelect().click();
cy.getByTestId('user-email')
.filter(':visible')
.should('have.length', 3)
.contains(INSTANCE_ADMIN.email)
.should('have.length', 1);
getVisibleSelect().contains(INSTANCE_OWNER.email.toLowerCase()).click();
credentialsModal.actions.addUser(INSTANCE_MEMBERS[1].email);
credentialsModal.actions.addUser(INSTANCE_ADMIN.email);
credentialsModal.actions.saveSharing();
credentialsModal.actions.close();
});
}); });

View file

@ -1,6 +1,7 @@
import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants'; import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN } from '../constants';
import { SettingsUsersPage, WorkflowPage } from '../pages'; import { MainSidebar, SettingsSidebar, SettingsUsersPage, WorkflowPage } from '../pages';
import { PersonalSettingsPage } from '../pages/settings-personal'; import { PersonalSettingsPage } from '../pages/settings-personal';
import { getVisibleSelect } from '../utils';
/** /**
* User A - Instance owner * User A - Instance owner
@ -25,9 +26,13 @@ const updatedPersonalData = {
const usersSettingsPage = new SettingsUsersPage(); const usersSettingsPage = new SettingsUsersPage();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const personalSettingsPage = new PersonalSettingsPage(); const personalSettingsPage = new PersonalSettingsPage();
const settingsSidebar = new SettingsSidebar();
const mainSidebar = new MainSidebar();
describe('User Management', { disableAutoLogin: true }, () => { describe('User Management', { disableAutoLogin: true }, () => {
before(() => cy.enableFeature('sharing')); before(() => {
cy.enableFeature('sharing');
});
it('should prevent non-owners to access UM settings', () => { it('should prevent non-owners to access UM settings', () => {
usersSettingsPage.actions.loginAndVisit( usersSettingsPage.actions.loginAndVisit(
@ -44,7 +49,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
it('should properly render UM settings page for instance owners', () => { it('should properly render UM settings page for instance owners', () => {
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true); usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
// All items in user list should be there // All items in user list should be there
usersSettingsPage.getters.userListItems().should('have.length', 3); usersSettingsPage.getters.userListItems().should('have.length', 4);
// List item for current user should have the `Owner` badge // List item for current user should have the `Owner` badge
usersSettingsPage.getters usersSettingsPage.getters
.userItem(INSTANCE_OWNER.email) .userItem(INSTANCE_OWNER.email)
@ -53,6 +58,93 @@ describe('User Management', { disableAutoLogin: true }, () => {
// Other users list items should contain action pop-up list // Other users list items should contain action pop-up list
usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[0].email).should('exist'); usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[0].email).should('exist');
usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[1].email).should('exist'); usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[1].email).should('exist');
usersSettingsPage.getters.userActionsToggle(INSTANCE_ADMIN.email).should('exist');
});
it('should be able to change user role to Admin and back', () => {
cy.enableFeature('advancedPermissions');
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
// Change role from Member to Admin
usersSettingsPage.getters
.userRoleSelect(INSTANCE_MEMBERS[0].email)
.find('input')
.should('contain.value', 'Member');
usersSettingsPage.getters.userRoleSelect(INSTANCE_MEMBERS[0].email).click();
getVisibleSelect().find('li').contains('Admin').click();
usersSettingsPage.getters
.userRoleSelect(INSTANCE_MEMBERS[0].email)
.find('input')
.should('contain.value', 'Admin');
usersSettingsPage.actions.loginAndVisit(
INSTANCE_MEMBERS[0].email,
INSTANCE_MEMBERS[0].password,
true,
);
// Change role from Admin to Member, then back to Admin
usersSettingsPage.getters
.userRoleSelect(INSTANCE_ADMIN.email)
.find('input')
.should('contain.value', 'Admin');
usersSettingsPage.getters.userRoleSelect(INSTANCE_ADMIN.email).click();
getVisibleSelect().find('li').contains('Member').click();
usersSettingsPage.getters
.userRoleSelect(INSTANCE_ADMIN.email)
.find('input')
.should('contain.value', 'Member');
usersSettingsPage.actions.loginAndVisit(INSTANCE_ADMIN.email, INSTANCE_ADMIN.password, false);
usersSettingsPage.actions.loginAndVisit(
INSTANCE_MEMBERS[0].email,
INSTANCE_MEMBERS[0].password,
true,
);
usersSettingsPage.getters.userRoleSelect(INSTANCE_ADMIN.email).click();
getVisibleSelect().find('li').contains('Admin').click();
usersSettingsPage.getters
.userRoleSelect(INSTANCE_ADMIN.email)
.find('input')
.should('contain.value', 'Admin');
usersSettingsPage.actions.loginAndVisit(INSTANCE_ADMIN.email, INSTANCE_ADMIN.password, true);
usersSettingsPage.getters.userRoleSelect(INSTANCE_MEMBERS[0].email).click();
getVisibleSelect().find('li').contains('Member').click();
usersSettingsPage.getters
.userRoleSelect(INSTANCE_MEMBERS[0].email)
.find('input')
.should('contain.value', 'Member');
cy.disableFeature('advancedPermissions');
});
it('should be able to change theme', () => {
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
personalSettingsPage.actions.changeTheme('Dark');
cy.get('body').should('have.attr', 'data-theme', 'dark');
settingsSidebar.actions.back();
mainSidebar.getters
.logo()
.should('have.attr', 'src')
.then((src) => {
expect(src).to.include('/n8n-dev-logo-dark-mode.svg');
});
cy.visit(personalSettingsPage.url);
personalSettingsPage.actions.changeTheme('Light');
cy.get('body').should('have.attr', 'data-theme', 'light');
settingsSidebar.actions.back();
mainSidebar.getters
.logo()
.should('have.attr', 'src')
.then((src) => {
expect(src).to.include('/n8n-dev-logo.svg');
});
}); });
it('should delete user and their data', () => { it('should delete user and their data', () => {

View file

@ -1,7 +1,8 @@
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { NDV, WorkflowPage as WorkflowPageClass, WorkflowsPage } from '../pages'; import { NDV, WorkflowExecutionsTab, WorkflowPage as WorkflowPageClass } from '../pages';
const workflowPage = new WorkflowPageClass(); const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab();
const ndv = new NDV(); const ndv = new NDV();
describe('Execution', () => { describe('Execution', () => {
@ -112,10 +113,6 @@ describe('Execution', () => {
.canvasNodeByName('Manual') .canvasNodeByName('Manual')
.within(() => cy.get('.fa-check')) .within(() => cy.get('.fa-check'))
.should('exist'); .should('exist');
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-check'))
.should('exist');
workflowPage.getters workflowPage.getters
.canvasNodeByName('Wait') .canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.visible')); .within(() => cy.get('.fa-sync-alt').should('not.visible'));
@ -191,10 +188,6 @@ describe('Execution', () => {
.canvasNodeByName('Webhook') .canvasNodeByName('Webhook')
.within(() => cy.get('.fa-check')) .within(() => cy.get('.fa-check'))
.should('exist'); .should('exist');
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-check'))
.should('exist');
workflowPage.getters workflowPage.getters
.canvasNodeByName('Set') .canvasNodeByName('Set')
.within(() => cy.get('.fa-check')) .within(() => cy.get('.fa-check'))
@ -267,10 +260,6 @@ describe('Execution', () => {
.canvasNodeByName('Webhook') .canvasNodeByName('Webhook')
.within(() => cy.get('.fa-check')) .within(() => cy.get('.fa-check'))
.should('exist'); .should('exist');
workflowPage.getters
.canvasNodeByName('Wait')
.within(() => cy.get('.fa-check'))
.should('exist');
workflowPage.getters workflowPage.getters
.canvasNodeByName('Wait') .canvasNodeByName('Wait')
.within(() => cy.get('.fa-sync-alt').should('not.visible')); .within(() => cy.get('.fa-sync-alt').should('not.visible'));
@ -286,4 +275,139 @@ describe('Execution', () => {
// Check success toast (works because Cypress waits enough for the element to show after the http request node has finished) // Check success toast (works because Cypress waits enough for the element to show after the http request node has finished)
workflowPage.getters.successToast().should('be.visible'); workflowPage.getters.successToast().should('be.visible');
}); });
describe('execution preview', () => {
it('when deleting the last execution, it should show empty state', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
workflowPage.actions.executeWorkflow();
executionsTab.actions.switchToExecutionsTab();
executionsTab.actions.deleteExecutionInPreview();
executionsTab.getters.successfulExecutionListItems().should('have.length', 0);
workflowPage.getters.successToast().contains('Execution deleted');
});
});
describe('connections should be colored differently for pinned data', () => {
beforeEach(() => {
cy.createFixtureWorkflow('Schedule_pinned.json', `Schedule pinned ${uuid()}`);
workflowPage.actions.deselectAll();
workflowPage.getters.zoomToFitButton().click();
workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('not.have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields1')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('not.have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields5', 'Edit Fields6')
.should('not.have.class', 'success')
.should('not.have.class', 'pinned');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields7', 'Edit Fields9')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('not.have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields1', 'Edit Fields2')
.should('not.have.class', 'success')
.should('not.have.class', 'pinned');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields2', 'Edit Fields3')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('not.have.class', 'has-run');
});
it('when executing the workflow', () => {
workflowPage.actions.executeWorkflow();
workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields1')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields5', 'Edit Fields6')
.should('have.class', 'success')
.should('not.have.class', 'pinned')
.should('not.have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields7', 'Edit Fields9')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields1', 'Edit Fields2')
.should('have.class', 'success')
.should('not.have.class', 'pinned')
.should('not.have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields2', 'Edit Fields3')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
});
it('when executing a node', () => {
workflowPage.actions.executeNode('Edit Fields3');
workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Schedule Trigger', 'Edit Fields1')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields5', 'Edit Fields6')
.should('not.have.class', 'success')
.should('not.have.class', 'pinned')
.should('not.have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields7', 'Edit Fields9')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('not.have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields1', 'Edit Fields2')
.should('have.class', 'success')
.should('not.have.class', 'pinned')
.should('not.have.class', 'has-run');
workflowPage.getters
.getConnectionBetweenNodes('Edit Fields2', 'Edit Fields3')
.should('have.class', 'success')
.should('have.class', 'pinned')
.should('have.class', 'has-run');
});
});
}); });

View file

@ -42,7 +42,7 @@ describe('Credentials', () => {
credentialsPage.getters.credentialCards().should('have.length', 1); credentialsPage.getters.credentialCards().should('have.length', 1);
}); });
it('should create a new credential using Add Credential button', () => { it.skip('should create a new credential using Add Credential button', () => {
credentialsPage.getters.createCredentialButton().click(); credentialsPage.getters.createCredentialButton().click();
credentialsModal.getters.newCredentialModal().should('be.visible'); credentialsModal.getters.newCredentialModal().should('be.visible');
@ -60,7 +60,7 @@ describe('Credentials', () => {
credentialsPage.getters.credentialCards().should('have.length', 2); credentialsPage.getters.credentialCards().should('have.length', 2);
}); });
it('should search credentials', () => { it.skip('should search credentials', () => {
// Search by name // Search by name
credentialsPage.actions.search('Notion'); credentialsPage.actions.search('Notion');
credentialsPage.getters.credentialCards().should('have.length', 1); credentialsPage.getters.credentialCards().should('have.length', 1);

View file

@ -1,18 +1,20 @@
import { WorkflowPage } from '../pages'; import { WorkflowPage } from '../pages';
import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab'; import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab';
import type { RouteHandler } from 'cypress/types/net-stubbing';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const executionsTab = new WorkflowExecutionsTab(); const executionsTab = new WorkflowExecutionsTab();
const executionsRefreshInterval = 4000;
// Test suite for executions tab // Test suite for executions tab
describe('Current Workflow Executions', () => { describe('Current Workflow Executions', () => {
beforeEach(() => { beforeEach(() => {
workflowPage.actions.visit(); workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`); cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`);
createMockExecutions();
}); });
it('should render executions tab correctly', () => { it('should render executions tab correctly', () => {
createMockExecutions();
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions'); cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
@ -29,6 +31,45 @@ describe('Current Workflow Executions', () => {
.invoke('attr', 'class') .invoke('attr', 'class')
.should('match', /_active_/); .should('match', /_active_/);
}); });
it('should not redirect back to execution tab when request is not done before leaving the page', () => {
cy.intercept('GET', '/rest/executions?filter=*');
cy.intercept('GET', '/rest/executions-current?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 = (req) => {
return new Promise((resolve) => {
setTimeout(() => resolve(req.continue()), 2000);
});
};
cy.intercept('GET', '/rest/executions?filter=*', throttleResponse);
cy.intercept('GET', '/rest/executions-current?filter=*', throttleResponse);
executionsTab.actions.switchToExecutionsTab();
executionsTab.actions.switchToEditorTab();
cy.wait(executionsRefreshInterval);
cy.url().should('not.include', '/executions');
});
}); });
const createMockExecutions = () => { const createMockExecutions = () => {

View file

@ -19,7 +19,7 @@ describe('NDV', () => {
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Item Lists'); workflowPage.actions.openNode('Sort');
ndv.getters.inputPanel().contains('6 items').should('exist'); ndv.getters.inputPanel().contains('6 items').should('exist');
ndv.getters.outputPanel().contains('6 items').should('exist'); ndv.getters.outputPanel().contains('6 items').should('exist');
@ -92,7 +92,7 @@ describe('NDV', () => {
ndv.getters.outputHoveringItem().should('have.text', '1000'); ndv.getters.outputHoveringItem().should('have.text', '1000');
ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); ndv.getters.parameterExpressionPreview('value').should('include.text', '1000');
ndv.actions.selectInputNode('Item Lists'); ndv.actions.selectInputNode('Sort');
ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); ndv.actions.changeOutputRunSelector('1 of 2 (6 items)');
ndv.getters.backToCanvas().realHover(); // reset to default hover ndv.getters.backToCanvas().realHover(); // reset to default hover

View file

@ -1,4 +1,7 @@
import { META_KEY } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { getPopper } from '../utils';
import { Interception } from 'cypress/types/net-stubbing';
const workflowPage = new WorkflowPageClass(); const workflowPage = new WorkflowPageClass();
@ -29,6 +32,19 @@ describe('Canvas Actions', () => {
workflowPage.getters.addStickyButton().should('not.be.visible'); workflowPage.getters.addStickyButton().should('not.be.visible');
addDefaultSticky(); addDefaultSticky();
workflowPage.actions.deselectAll();
workflowPage.actions.addStickyFromContextMenu();
workflowPage.actions.hitAddStickyShortcut();
workflowPage.getters.stickies().should('have.length', 3);
// Should not add a sticky for ctrl+shift+s
cy.get('body')
.type(META_KEY, { delay: 500, release: false })
.type('{shift}', { release: false })
.type('s');
workflowPage.getters.stickies().should('have.length', 3);
workflowPage.getters workflowPage.getters
.stickies() .stickies()
.eq(0) .eq(0)
@ -66,6 +82,32 @@ describe('Canvas Actions', () => {
workflowPage.getters.stickies().should('have.length', 0); workflowPage.getters.stickies().should('have.length', 0);
}); });
it('change sticky color', () => {
workflowPage.actions.addSticky();
workflowPage.getters.stickies().should('have.length', 1);
workflowPage.actions.toggleColorPalette();
getPopper().should('be.visible');
workflowPage.actions.pickColor(2);
workflowPage.actions.toggleColorPalette();
getPopper().should('not.be.visible');
workflowPage.actions.saveWorkflowOnButtonClick();
cy.wait('@createWorkflow').then((interception: Interception) => {
const { request } = interception;
const color = request.body?.nodes[0]?.parameters?.color;
expect(color).to.equal(2);
});
workflowPage.getters.stickies().should('have.length', 1);
});
it('edits sticky and updates content as markdown', () => { it('edits sticky and updates content as markdown', () => {
workflowPage.actions.addSticky(); workflowPage.actions.addSticky();
@ -84,8 +126,11 @@ describe('Canvas Actions', () => {
moveSticky({ top: 200, left: 200 }); moveSticky({ top: 200, left: 200 });
dragRightEdge({ left: 200, top: 200, height: 160, width: 240 }, 100); cy.drag('[data-test-id="sticky"] [data-dir="right"]', [100, 100]);
dragRightEdge({ left: 200, top: 200, height: 160, width: 240 }, -50); checkStickiesStyle(100, 20, 160, 346);
cy.drag('[data-test-id="sticky"] [data-dir="right"]', [-50, -50]);
checkStickiesStyle(100, 20, 160, 302);
}); });
it('expands/shrinks sticky from the left edge', () => { it('expands/shrinks sticky from the left edge', () => {
@ -205,27 +250,6 @@ type Position = {
left: number; left: number;
}; };
type BoundingBox = {
height: number;
width: number;
top: number;
left: number;
};
function dragRightEdge(curr: BoundingBox, move: number) {
workflowPage.getters
.stickies()
.first()
.then(($el) => {
const { left, top, height, width } = curr;
cy.drag(`[data-test-id="sticky"] [data-dir="right"]`, [left + width + move, 0], {
abs: true,
});
stickyShouldBePositionedCorrectly({ top, left });
stickyShouldHaveCorrectSize([height, width * 1.5 + move]);
});
}
function shouldHaveOneSticky() { function shouldHaveOneSticky() {
workflowPage.getters.stickies().should('have.length', 1); workflowPage.getters.stickies().should('have.length', 1);
} }

View file

@ -1,5 +1,5 @@
import { WorkflowPage, NDV, CredentialsModal } from '../pages'; import { WorkflowPage, NDV, CredentialsModal } from '../pages';
import { getPopper, getVisiblePopper, getVisibleSelect } from '../utils'; import { getVisiblePopper, getVisibleSelect } from '../utils';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -16,7 +16,7 @@ describe('Resource Locator', () => {
it('should render both RLC components in google sheets', () => { it('should render both RLC components in google sheets', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual'); workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true); workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
ndv.getters.resourceLocator('documentId').should('be.visible'); ndv.getters.resourceLocator('documentId').should('be.visible');
ndv.getters.resourceLocator('sheetName').should('be.visible'); ndv.getters.resourceLocator('sheetName').should('be.visible');
ndv.getters ndv.getters
@ -31,7 +31,7 @@ describe('Resource Locator', () => {
it('should show appropriate error when credentials are not set', () => { it('should show appropriate error when credentials are not set', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual'); workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true); workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
ndv.getters.resourceLocator('documentId').should('be.visible'); ndv.getters.resourceLocator('documentId').should('be.visible');
ndv.getters.resourceLocatorInput('documentId').click(); ndv.getters.resourceLocatorInput('documentId').click();
ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE); ndv.getters.resourceLocatorErrorMessage().should('contain', NO_CREDENTIALS_MESSAGE);
@ -39,7 +39,7 @@ describe('Resource Locator', () => {
it('should show appropriate error when credentials are not valid', () => { it('should show appropriate error when credentials are not valid', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual'); workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true); workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
workflowPage.getters.nodeCredentialsSelect().click(); workflowPage.getters.nodeCredentialsSelect().click();
// Add oAuth credentials // Add oAuth credentials
getVisibleSelect().find('li').last().click(); getVisibleSelect().find('li').last().click();
@ -54,7 +54,7 @@ describe('Resource Locator', () => {
it('should reset resource locator when dependent field is changed', () => { it('should reset resource locator when dependent field is changed', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual'); workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Google Sheets', true, true); workflowPage.actions.addNodeToCanvas('Google Sheets', true, true, 'Update row in sheet');
ndv.actions.setRLCValue('documentId', '123'); ndv.actions.setRLCValue('documentId', '123');
ndv.actions.setRLCValue('sheetName', '123'); ndv.actions.setRLCValue('sheetName', '123');
ndv.actions.setRLCValue('documentId', '321'); ndv.actions.setRLCValue('documentId', '321');
@ -66,6 +66,8 @@ describe('Resource Locator', () => {
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Resource Locator' }); workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Resource Locator' });
ndv.getters.resourceLocatorInput('rlc').click(); ndv.getters.resourceLocatorInput('rlc').click();
cy.getByTestId('rlc-item').should('exist');
getVisiblePopper() getVisiblePopper()
.should('have.length', 1) .should('have.length', 1)
.findChildByTestId('rlc-item') .findChildByTestId('rlc-item')
@ -73,9 +75,11 @@ describe('Resource Locator', () => {
ndv.actions.setInvalidExpression({ fieldName: 'fieldId' }); ndv.actions.setInvalidExpression({ fieldName: 'fieldId' });
ndv.getters.container().click(); // remove focus from input, hide expression preview ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview
ndv.getters.resourceLocatorInput('rlc').click(); ndv.getters.resourceLocatorInput('rlc').click();
cy.getByTestId('rlc-item').should('exist');
getVisiblePopper() getVisiblePopper()
.should('have.length', 1) .should('have.length', 1)
.findChildByTestId('rlc-item') .findChildByTestId('rlc-item')

119
cypress/e2e/27-cloud.cy.ts Normal file
View file

@ -0,0 +1,119 @@
import {
BannerStack,
MainSidebar,
WorkflowPage,
visitPublicApiPage,
getPublicApiUpgradeCTA,
} from '../pages';
import planData from '../fixtures/Plan_data_opt_in_trial.json';
import { INSTANCE_OWNER } from '../constants';
const mainSidebar = new MainSidebar();
const bannerStack = new BannerStack();
const workflowPage = new WorkflowPage();
describe('Cloud', { disableAutoLogin: true }, () => {
before(() => {
const now = new Date();
const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
planData.expirationDate = fiveDaysFromNow.toJSON();
});
describe('BannerStack', () => {
it('should render trial banner for opt-in cloud user', () => {
cy.intercept('GET', '/rest/admin/cloud-plan', {
body: planData,
}).as('getPlanData');
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } },
});
});
}).as('loadSettings');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.visit(workflowPage.url);
cy.wait('@getPlanData');
bannerStack.getters.banner().should('be.visible');
mainSidebar.actions.signout();
bannerStack.getters.banner().should('not.be.visible');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.visit(workflowPage.url);
bannerStack.getters.banner().should('be.visible');
mainSidebar.actions.signout();
});
it('should not render opt-in-trial banner for non cloud deployment', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'default' } },
});
});
}).as('loadSettings');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.visit(workflowPage.url);
bannerStack.getters.banner().should('not.be.visible');
mainSidebar.actions.signout();
});
});
describe('Admin Home', () => {
it('Should show admin button', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } },
});
});
}).as('loadSettings');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.visit(workflowPage.url);
mainSidebar.getters.adminPanel().should('be.visible');
});
});
describe('Public API', () => {
it('Should show upgrade CTA for Public API if user is trialing', () => {
cy.intercept('GET', '/rest/admin/cloud-plan', {
body: planData,
}).as('getPlanData');
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: {
...res.body.data,
deployment: { type: 'cloud' },
n8nMetadata: { userId: 1 },
},
});
});
}).as('loadSettings');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
visitPublicApiPage();
getPublicApiUpgradeCTA().should('be.visible');
});
});
});

View file

@ -1,67 +0,0 @@
import { BannerStack, MainSidebar, WorkflowPage } from '../pages';
import planData from '../fixtures/Plan_data_opt_in_trial.json';
import { INSTANCE_OWNER } from '../constants';
const mainSidebar = new MainSidebar();
const bannerStack = new BannerStack();
const workflowPage = new WorkflowPage();
describe('BannerStack', { disableAutoLogin: true }, () => {
before(() => {
const now = new Date();
const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
planData.expirationDate = fiveDaysFromNow.toJSON();
});
it('should render trial banner for opt-in cloud user', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' }, n8nMetadata: { userId: 1 } },
});
});
}).as('loadSettings');
cy.intercept('GET', '/rest/admin/cloud-plan', {
body: planData,
}).as('getPlanData');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.visit(workflowPage.url);
cy.wait('@getPlanData');
bannerStack.getters.banner().should('be.visible');
mainSidebar.actions.signout();
bannerStack.getters.banner().should('not.be.visible');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.visit(workflowPage.url);
bannerStack.getters.banner().should('be.visible');
mainSidebar.actions.signout();
});
it('should not render opt-in-trial banner for non cloud deployment', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'default' } },
});
});
}).as('loadSettings');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.visit(workflowPage.url);
bannerStack.getters.banner().should('not.be.visible');
mainSidebar.actions.signout();
});
});

View file

@ -1,5 +1,5 @@
import { MainSidebar } from './../pages/sidebar/main-sidebar'; import { MainSidebar } from './../pages/sidebar/main-sidebar';
import { INSTANCE_OWNER, BACKEND_BASE_URL } from '../constants'; import { INSTANCE_OWNER, INSTANCE_ADMIN, BACKEND_BASE_URL } from '../constants';
import { SigninPage } from '../pages'; import { SigninPage } from '../pages';
import { PersonalSettingsPage } from '../pages/settings-personal'; import { PersonalSettingsPage } from '../pages/settings-personal';
import { MfaLoginPage } from '../pages/mfa-login'; import { MfaLoginPage } from '../pages/mfa-login';
@ -19,6 +19,16 @@ const user = {
mfaRecoveryCodes: [RECOVERY_CODE], mfaRecoveryCodes: [RECOVERY_CODE],
}; };
const admin = {
email: INSTANCE_ADMIN.email,
password: INSTANCE_ADMIN.password,
firstName: 'Admin',
lastName: 'B',
mfaEnabled: false,
mfaSecret: MFA_SECRET,
mfaRecoveryCodes: [RECOVERY_CODE],
};
const mfaLoginPage = new MfaLoginPage(); const mfaLoginPage = new MfaLoginPage();
const signinPage = new SigninPage(); const signinPage = new SigninPage();
const personalSettingsPage = new PersonalSettingsPage(); const personalSettingsPage = new PersonalSettingsPage();
@ -30,6 +40,7 @@ describe('Two-factor authentication', () => {
cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, { cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, {
owner: user, owner: user,
members: [], members: [],
admin,
}); });
cy.on('uncaught:exception', (err, runnable) => { cy.on('uncaught:exception', (err, runnable) => {
expect(err.message).to.include('Not logged in'); expect(err.message).to.include('Not logged in');

View file

@ -12,14 +12,11 @@ const ndv = new NDV();
const executionsTab = new WorkflowExecutionsTab(); const executionsTab = new WorkflowExecutionsTab();
describe('Debug', () => { describe('Debug', () => {
beforeEach(() => {
cy.enableFeature('debugInEditor');
});
it('should be able to debug executions', () => { it('should be able to debug executions', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, enterprise: { debugInEditor: true } },
});
});
}).as('loadSettings');
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions'); cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution'); cy.intercept('GET', '/rest/executions/*').as('getExecution');
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions'); cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
@ -47,6 +44,7 @@ describe('Debug', () => {
cy.wait(['@getExecutions', '@getCurrentExecutions']); cy.wait(['@getExecutions', '@getCurrentExecutions']);
executionsTab.getters.executionDebugButton().should('have.text', 'Debug in editor').click(); executionsTab.getters.executionDebugButton().should('have.text', 'Debug in editor').click();
cy.url().should('include', '/debug');
cy.get('.el-notification').contains('Execution data imported').should('be.visible'); cy.get('.el-notification').contains('Execution data imported').should('be.visible');
cy.get('.matching-pinned-nodes-confirmation').should('not.exist'); cy.get('.matching-pinned-nodes-confirmation').should('not.exist');
@ -56,6 +54,8 @@ describe('Debug', () => {
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.saveWorkflowUsingKeyboardShortcut(); workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
cy.url().should('not.include', '/debug');
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
cy.wait(['@postWorkflowRun']); cy.wait(['@postWorkflowRun']);
@ -87,6 +87,7 @@ describe('Debug', () => {
confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible'); confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible');
confirmDialog.find('li').should('have.length', 2); confirmDialog.find('li').should('have.length', 2);
confirmDialog.get('.btn--confirm').click(); confirmDialog.get('.btn--confirm').click();
cy.url().should('include', '/debug');
workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon'); workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon');
workflowPage.getters workflowPage.getters
@ -104,6 +105,7 @@ describe('Debug', () => {
workflowPage.getters.canvasNodePlusEndpointByName(EDIT_FIELDS_SET_NODE_NAME).click(); workflowPage.getters.canvasNodePlusEndpointByName(EDIT_FIELDS_SET_NODE_NAME).click();
workflowPage.actions.addNodeToCanvas(IF_NODE_NAME, false); workflowPage.actions.addNodeToCanvas(IF_NODE_NAME, false);
workflowPage.actions.saveWorkflowUsingKeyboardShortcut(); workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
cy.url().should('not.include', '/debug');
executionsTab.actions.switchToExecutionsTab(); executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']); cy.wait(['@getExecutions', '@getCurrentExecutions']);
@ -112,6 +114,8 @@ describe('Debug', () => {
confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible'); confirmDialog = cy.get('.matching-pinned-nodes-confirmation').filter(':visible');
confirmDialog.find('li').should('have.length', 1); confirmDialog.find('li').should('have.length', 1);
confirmDialog.get('.btn--confirm').click(); confirmDialog.get('.btn--confirm').click();
cy.url().should('include', '/debug');
workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty'); workflowPage.getters.canvasNodes().last().find('.node-info-icon').should('be.empty');
workflowPage.getters.canvasNodes().first().dblclick(); workflowPage.getters.canvasNodes().first().dblclick();
@ -119,7 +123,10 @@ describe('Debug', () => {
ndv.actions.close(); ndv.actions.close();
workflowPage.actions.saveWorkflowUsingKeyboardShortcut(); workflowPage.actions.saveWorkflowUsingKeyboardShortcut();
cy.url().should('not.include', '/debug');
workflowPage.actions.executeWorkflow(); workflowPage.actions.executeWorkflow();
workflowPage.actions.zoomToFit();
workflowPage.actions.deleteNode(IF_NODE_NAME); workflowPage.actions.deleteNode(IF_NODE_NAME);
executionsTab.actions.switchToExecutionsTab(); executionsTab.actions.switchToExecutionsTab();
@ -128,5 +135,6 @@ describe('Debug', () => {
cy.wait(['@getExecution']); cy.wait(['@getExecution']);
executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click(); executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click();
cy.get('.el-notification').contains("Some execution data wasn't imported").should('be.visible'); cy.get('.el-notification').contains("Some execution data wasn't imported").should('be.visible');
cy.url().should('include', '/debug');
}); });
}); });

View file

@ -2,11 +2,21 @@ import { TemplatesPage } from '../pages/templates';
import { WorkflowPage } from '../pages/workflow'; import { WorkflowPage } from '../pages/workflow';
import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json'; import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json';
import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json';
import { TemplateWorkflowPage } from '../pages/template-workflow';
const templatesPage = new TemplatesPage(); const templatesPage = new TemplatesPage();
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const templateWorkflowPage = new TemplateWorkflowPage();
describe('Templates', () => { describe('Templates', () => {
beforeEach(() => {
cy.intercept('GET', '**/api/templates/search?page=1&rows=20&category=&search=', { fixture: 'templates_search/all_templates_search_response.json' }).as('searchRequest');
cy.intercept('GET', '**/api/templates/search?page=1&rows=20&category=Sales*', { fixture: 'templates_search/sales_templates_search_response.json' }).as('categorySearchRequest');
cy.intercept('GET', '**/api/templates/workflows/*', { fixture: 'templates_search/test_template_preview.json' }).as('singleTemplateRequest');
cy.intercept('GET', '**/api/workflows/templates/*', { fixture: 'templates_search/test_template_import.json' }).as('singleTemplateRequest');
});
it('can open onboarding flow', () => { it('can open onboarding flow', () => {
templatesPage.actions.openOnboardingFlow(1234, OnboardingWorkflow.name, OnboardingWorkflow); templatesPage.actions.openOnboardingFlow(1234, OnboardingWorkflow.name, OnboardingWorkflow);
cy.url().then(($url) => { cy.url().then(($url) => {
@ -31,4 +41,99 @@ describe('Templates', () => {
workflowPage.getters.stickies().should('have.length', 1); workflowPage.getters.stickies().should('have.length', 1);
workflowPage.actions.shouldHaveWorkflowName(OnboardingWorkflow.name); workflowPage.actions.shouldHaveWorkflowName(OnboardingWorkflow.name);
}); });
it('should save template id with the workflow', () => {
cy.visit(templatesPage.url);
cy.get('.el-skeleton.n8n-loading').should('not.exist');
templatesPage.getters.firstTemplateCard().should('exist');
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.firstTemplateCard().click();
cy.url().should('include', '/templates/');
cy.url().then(($url) => {
const templateId = $url.split('/').pop();
templatesPage.getters.useTemplateButton().click();
cy.url().should('include', '/workflow/new');
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.selectAll();
workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
// Check workflow JSON by copying it to clipboard
cy.readClipboard().then((workflowJSON) => {
expect(workflowJSON).to.contain(`"templateId": "${templateId}"`);
});
});
});
it('can open template with images and hides workflow screenshots', () => {
templateWorkflowPage.actions.openTemplate(WorkflowTemplate);
templateWorkflowPage.getters.description().find('img').should('have.length', 1);
});
it('renders search elements correctly', () => {
cy.visit(templatesPage.url);
templatesPage.getters.searchInput().should('exist');
templatesPage.getters.allCategoriesFilter().should('exist');
templatesPage.getters.categoryFilters().should('have.length.greaterThan', 1);
templatesPage.getters.templateCards().should('have.length.greaterThan', 0);
});
it('can filter templates by category', () => {
cy.visit(templatesPage.url);
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.expandCategoriesButton().click();
templatesPage.getters.categoryFilter('sales').should('exist');
let initialTemplateCount = 0;
let initialCollectionCount = 0;
templatesPage.getters.templateCountLabel().then(($el) => {
initialTemplateCount = parseInt($el.text().replace(/\D/g, ''), 10);
templatesPage.getters.collectionCountLabel().then(($el) => {
initialCollectionCount = parseInt($el.text().replace(/\D/g, ''), 10);
templatesPage.getters.categoryFilter('sales').click();
templatesPage.getters.templatesLoadingContainer().should('not.exist');
// Should have less templates and collections after selecting a category
templatesPage.getters.templateCountLabel().should(($el) => {
expect(parseInt($el.text().replace(/\D/g, ''), 10)).to.be.lessThan(initialTemplateCount);
});
templatesPage.getters.collectionCountLabel().should(($el) => {
expect(parseInt($el.text().replace(/\D/g, ''), 10)).to.be.lessThan(initialCollectionCount);
});
});
});
});
it('should preserve search query in URL', () => {
cy.visit(templatesPage.url);
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.expandCategoriesButton().click();
templatesPage.getters.categoryFilter('sales').should('exist');
templatesPage.getters.categoryFilter('sales').click();
templatesPage.getters.searchInput().type('auto');
cy.url().should('include', '?categories=');
cy.url().should('include', '&search=');
cy.reload();
// Should preserve search query in URL
cy.url().should('include', '?categories=');
cy.url().should('include', '&search=');
// Sales category should still be selected
templatesPage.getters.categoryFilter('sales').find('label').should('have.class', 'is-checked');
// Search input should still have the search query
templatesPage.getters.searchInput().should('have.value', 'auto');
// Sales checkbox should be pushed to the top
templatesPage.getters.categoryFilters().eq(1).then(($el) => {
expect($el.text()).to.equal('Sales');
});
});
}); });

View file

@ -0,0 +1,218 @@
import {
CODE_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
IF_NODE_NAME,
INSTANCE_OWNER,
SCHEDULE_TRIGGER_NODE_NAME,
} from '../constants';
import {
WorkflowExecutionsTab,
WorkflowPage as WorkflowPageClass,
WorkflowHistoryPage,
} from '../pages';
const workflowPage = new WorkflowPageClass();
const executionsTab = new WorkflowExecutionsTab();
const workflowHistoryPage = new WorkflowHistoryPage();
const createNewWorkflowAndActivate = () => {
workflowPage.actions.visit();
workflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.activateWorkflow();
cy.get('.el-notification .el-notification--error').should('not.exist');
};
const editWorkflowAndDeactivate = () => {
workflowPage.getters.canvasNodePlusEndpointByName(SCHEDULE_TRIGGER_NODE_NAME).click();
workflowPage.getters.nodeCreatorSearchBar().should('be.visible');
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
cy.get('.jtk-connector').should('have.length', 1);
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.getters.activatorSwitch().click();
workflowPage.actions.zoomToFit();
cy.get('.el-notification .el-notification--error').should('not.exist');
};
const editWorkflowMoreAndActivate = () => {
cy.drag(workflowPage.getters.getEndpointSelector('plus', EDIT_FIELDS_SET_NODE_NAME), [200, 200], {
realMouse: true,
});
workflowPage.getters.nodeCreatorSearchBar().should('be.visible');
workflowPage.actions.addNodeToCanvas(CODE_NODE_NAME, false);
workflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
cy.get('.jtk-connector').should('have.length', 2);
workflowPage.actions.zoomToFit();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.addNodeToCanvas(IF_NODE_NAME);
workflowPage.getters.nodeViewBackground().click(600, 200, { force: true });
cy.get('.jtk-connector').should('have.length', 2);
const position = {
top: 0,
left: 0,
};
workflowPage.getters
.canvasNodeByName(IF_NODE_NAME)
.click()
.then(($element) => {
position.top = $element.position().top;
position.left = $element.position().left;
});
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 200], { clickToFinish: true });
workflowPage.getters
.canvasNodes()
.last()
.then(($element) => {
const finalPosition = {
top: $element.position().top,
left: $element.position().left,
};
expect(finalPosition.top).to.be.greaterThan(position.top);
expect(finalPosition.left).to.be.greaterThan(position.left);
});
cy.draganddrop(
workflowPage.getters.getEndpointSelector('output', CODE_NODE_NAME),
workflowPage.getters.getEndpointSelector('input', IF_NODE_NAME),
);
cy.get('.jtk-connector').should('have.length', 3);
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.getters.activatorSwitch().click();
cy.get('.el-notification .el-notification--error').should('not.exist');
};
const switchBetweenEditorAndHistory = () => {
workflowPage.getters.workflowHistoryButton().click();
cy.wait(['@getHistory']);
cy.wait(['@getVersion']);
cy.intercept('GET', '/rest/workflows/*').as('workflowGet');
workflowHistoryPage.getters.workflowHistoryCloseButton().click();
cy.wait(['@workflowGet']);
cy.wait(1000);
workflowPage.getters.canvasNodes().first().should('be.visible');
workflowPage.getters.canvasNodes().last().should('be.visible');
};
const switchBetweenEditorAndWorkflowlist = () => {
cy.getByTestId('menu-item').first().click();
cy.wait(['@getUsers', '@getWorkflows', '@getActiveWorkflows', '@getCredentials']);
cy.getByTestId('resources-list-item').first().click();
workflowPage.getters.canvasNodes().first().should('be.visible');
workflowPage.getters.canvasNodes().last().should('be.visible');
};
const zoomInAndCheckNodes = () => {
cy.getByTestId('zoom-in-button').click();
cy.getByTestId('zoom-in-button').click();
cy.getByTestId('zoom-in-button').click();
cy.getByTestId('zoom-in-button').click();
workflowPage.getters.canvasNodes().first().should('not.be.visible');
workflowPage.getters.canvasNodes().last().should('not.be.visible');
};
describe('Editor actions should work', () => {
beforeEach(() => {
cy.enableFeature('debugInEditor');
cy.enableFeature('workflowHistory');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
createNewWorkflowAndActivate();
});
it('after saving a new workflow', () => {
editWorkflowAndDeactivate();
editWorkflowMoreAndActivate();
});
it('after switching between Editor and Executions', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
cy.wait(500);
executionsTab.actions.switchToEditorTab();
editWorkflowAndDeactivate();
editWorkflowMoreAndActivate();
});
it('after switching between Editor and Debug', () => {
cy.intercept('GET', '/rest/executions?filter=*').as('getExecutions');
cy.intercept('GET', '/rest/executions/*').as('getExecution');
cy.intercept('GET', '/rest/executions-current?filter=*').as('getCurrentExecutions');
cy.intercept('POST', '/rest/workflows/run').as('postWorkflowRun');
editWorkflowAndDeactivate();
workflowPage.actions.executeWorkflow();
cy.wait(['@postWorkflowRun']);
executionsTab.actions.switchToExecutionsTab();
cy.wait(['@getExecutions', '@getCurrentExecutions']);
executionsTab.getters.executionListItems().should('have.length', 1).first().click();
cy.wait(['@getExecution']);
executionsTab.getters.executionDebugButton().should('have.text', 'Copy to editor').click();
editWorkflowMoreAndActivate();
});
it('after switching between Editor and Workflow history', () => {
cy.intercept('GET', '/rest/workflow-history/workflow/*/version/*').as('getVersion');
cy.intercept('GET', '/rest/workflow-history/workflow/*').as('getHistory');
editWorkflowAndDeactivate();
workflowPage.getters.workflowHistoryButton().click();
cy.wait(['@getHistory']);
cy.wait(['@getVersion']);
cy.intercept('GET', '/rest/workflows/*').as('workflowGet');
workflowHistoryPage.getters.workflowHistoryCloseButton().click();
cy.wait(['@workflowGet']);
cy.wait(1000);
editWorkflowMoreAndActivate();
});
});
describe('Editor zoom should work after route changes', () => {
beforeEach(() => {
cy.enableFeature('debugInEditor');
cy.enableFeature('workflowHistory');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
workflowPage.actions.visit();
cy.createFixtureWorkflow('Lots_of_nodes.json', `Lots of nodes`);
workflowPage.actions.saveWorkflowOnButtonClick();
});
it('after switching between Editor and Workflow history and Workflow list', () => {
cy.intercept('GET', '/rest/workflow-history/workflow/*/version/*').as('getVersion');
cy.intercept('GET', '/rest/workflow-history/workflow/*').as('getHistory');
cy.intercept('GET', '/rest/users').as('getUsers');
cy.intercept('GET', '/rest/workflows').as('getWorkflows');
cy.intercept('GET', '/rest/active-workflows').as('getActiveWorkflows');
cy.intercept('GET', '/rest/credentials').as('getCredentials');
switchBetweenEditorAndHistory();
zoomInAndCheckNodes();
switchBetweenEditorAndHistory();
switchBetweenEditorAndHistory();
zoomInAndCheckNodes();
switchBetweenEditorAndWorkflowlist();
zoomInAndCheckNodes();
switchBetweenEditorAndWorkflowlist();
switchBetweenEditorAndWorkflowlist();
zoomInAndCheckNodes();
switchBetweenEditorAndHistory();
switchBetweenEditorAndWorkflowlist();
});
});

View file

@ -0,0 +1,58 @@
import { IF_NODE_NAME } from '../constants';
import { WorkflowPage, NDV } from '../pages';
const workflowPage = new WorkflowPage();
const ndv = new NDV();
const FILTER_PARAM_NAME = 'conditions';
describe('If Node (filter component)', () => {
beforeEach(() => {
workflowPage.actions.visit();
});
it('should be able to create and delete multiple conditions', () => {
workflowPage.actions.addInitialNodeToCanvas(IF_NODE_NAME, { keepNdvOpen: true });
// Default state
ndv.getters.filterComponent(FILTER_PARAM_NAME).should('exist');
ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 1);
ndv.getters
.filterConditionOperator(FILTER_PARAM_NAME)
.find('input')
.should('have.value', 'is equal to');
// Add
ndv.actions.addFilterCondition(FILTER_PARAM_NAME);
ndv.getters.filterConditionLeft(FILTER_PARAM_NAME, 0).find('input').type('first left');
ndv.getters.filterConditionLeft(FILTER_PARAM_NAME, 1).find('input').type('second left');
ndv.actions.addFilterCondition(FILTER_PARAM_NAME);
ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 3);
// Delete
ndv.actions.removeFilterCondition(FILTER_PARAM_NAME, 0);
ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 2);
ndv.getters
.filterConditionLeft(FILTER_PARAM_NAME, 0)
.find('input')
.should('have.value', 'second left');
ndv.actions.removeFilterCondition(FILTER_PARAM_NAME, 1);
ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 1);
});
it('should correctly evaluate conditions', () => {
cy.fixture('Test_workflow_filter.json').then((data) => {
cy.get('body').paste(JSON.stringify(data));
});
workflowPage.actions.zoomToFit();
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Then');
ndv.getters.outputPanel().contains('3 items').should('exist');
ndv.actions.close();
workflowPage.actions.openNode('Else');
ndv.getters.outputPanel().contains('1 item').should('exist');
});
});

View file

@ -0,0 +1,278 @@
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,
} from './../constants';
import { createMockNodeExecutionData, runMockWorkflowExcution } from '../utils';
import {
addLanguageModelNodeToParent,
addMemoryNodeToParent,
addNodeToCanvas,
addOutputParserNodeToParent,
addToolNodeToParent,
clickManualChatButton,
navigateToNewWorkflowPage,
openNode,
} from '../composables/workflow';
import {
clickCreateNewCredential,
clickExecuteNode,
clickGetBackToCanvas,
getOutputPanelTable,
setParameterInputByName,
} from '../composables/ndv';
import { setCredentialValues } from '../composables/modals/credential-modal';
import {
closeManualChatModal,
getManualChatMessages,
getManualChatModalLogs,
getManualChatModalLogsEntries,
getManualChatModalLogsTree,
sendManualChatMessage,
} from '../composables/modals/chat-modal';
describe('Langchain Integration', () => {
beforeEach(() => {
navigateToNewWorkflowPage();
});
it('should add nodes to all Agent node input types', () => {
addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
addNodeToCanvas(AGENT_NODE_NAME, true);
addLanguageModelNodeToParent(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, AGENT_NODE_NAME);
clickGetBackToCanvas();
addMemoryNodeToParent(AI_MEMORY_WINDOW_BUFFER_MEMORY_NODE_NAME, AGENT_NODE_NAME);
clickGetBackToCanvas();
addToolNodeToParent(AI_TOOL_CALCULATOR_NODE_NAME, AGENT_NODE_NAME);
clickGetBackToCanvas();
addOutputParserNodeToParent(AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME, AGENT_NODE_NAME);
clickGetBackToCanvas();
});
it('should add multiple tool nodes to Agent node tool input type', () => {
addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME, true);
addNodeToCanvas(AGENT_NODE_NAME, true);
[
AI_TOOL_CALCULATOR_NODE_NAME,
AI_TOOL_CODE_NODE_NAME,
AI_TOOL_CODE_NODE_NAME,
AI_TOOL_CODE_NODE_NAME,
AI_TOOL_WIKIPEDIA_NODE_NAME,
].forEach((tool) => {
addToolNodeToParent(tool, AGENT_NODE_NAME);
clickGetBackToCanvas();
});
});
it('should be able to open and execute Basic LLM Chain node', () => {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
addNodeToCanvas(BASIC_LLM_CHAIN_NODE_NAME, true);
addLanguageModelNodeToParent(
AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME,
BASIC_LLM_CHAIN_NODE_NAME,
);
clickCreateNewCredential();
setCredentialValues({
apiKey: 'sk_test_123',
});
clickGetBackToCanvas();
openNode(BASIC_LLM_CHAIN_NODE_NAME);
const inputMessage = 'Hello!';
const outputMessage = 'Hi there! How can I assist you today?';
setParameterInputByName('prompt', inputMessage);
runMockWorkflowExcution({
trigger: () => clickExecuteNode(),
runData: [
createMockNodeExecutionData(BASIC_LLM_CHAIN_NODE_NAME, {
jsonData: {
main: { output: outputMessage },
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
}),
],
lastNodeExecuted: BASIC_LLM_CHAIN_NODE_NAME,
});
getOutputPanelTable().should('contain', 'output');
getOutputPanelTable().should('contain', outputMessage);
});
it('should be able to open and execute Agent node', () => {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
addNodeToCanvas(AGENT_NODE_NAME, true);
addLanguageModelNodeToParent(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, AGENT_NODE_NAME);
clickCreateNewCredential();
setCredentialValues({
apiKey: 'sk_test_123',
});
clickGetBackToCanvas();
openNode(AGENT_NODE_NAME);
const inputMessage = 'Hello!';
const outputMessage = 'Hi there! How can I assist you today?';
setParameterInputByName('text', inputMessage);
runMockWorkflowExcution({
trigger: () => clickExecuteNode(),
runData: [
createMockNodeExecutionData(AGENT_NODE_NAME, {
jsonData: {
main: { output: outputMessage },
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
}),
],
lastNodeExecuted: AGENT_NODE_NAME,
});
getOutputPanelTable().should('contain', 'output');
getOutputPanelTable().should('contain', outputMessage);
});
it('should add and use Manual Chat Trigger node together with Agent node', () => {
addNodeToCanvas(MANUAL_CHAT_TRIGGER_NODE_NAME, true);
addNodeToCanvas(AGENT_NODE_NAME, true);
addLanguageModelNodeToParent(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, AGENT_NODE_NAME);
clickCreateNewCredential();
setCredentialValues({
apiKey: 'sk_test_123',
});
clickGetBackToCanvas();
clickManualChatButton();
getManualChatModalLogs().should('not.exist');
const inputMessage = 'Hello!';
const outputMessage = 'Hi there! How can I assist you today?';
runMockWorkflowExcution({
trigger: () => {
sendManualChatMessage(inputMessage);
},
runData: [
createMockNodeExecutionData(MANUAL_CHAT_TRIGGER_NODE_NAME, {
jsonData: {
main: { input: inputMessage },
},
}),
createMockNodeExecutionData(AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, {
jsonData: {
ai_languageModel: {
response: {
generations: [
{
text: `{
"action": "Final Answer",
"action_input": "${outputMessage}"
}`,
message: {
lc: 1,
type: 'constructor',
id: ['langchain', 'schema', 'AIMessage'],
kwargs: {
content: `{
"action": "Final Answer",
"action_input": "${outputMessage}"
}`,
additional_kwargs: {},
},
},
generationInfo: { finish_reason: 'stop' },
},
],
llmOutput: {
tokenUsage: {
completionTokens: 26,
promptTokens: 519,
totalTokens: 545,
},
},
},
},
},
inputOverride: {
ai_languageModel: [
[
{
json: {
messages: [
{
lc: 1,
type: 'constructor',
id: ['langchain', 'schema', 'SystemMessage'],
kwargs: {
content:
'Assistant is a large language model trained by OpenAI.\n\nAssistant is designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, Assistant is able to generate human-like text based on the input it receives, allowing it to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.\n\nAssistant is constantly learning and improving, and its capabilities are constantly evolving. It is able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. Additionally, Assistant is able to generate its own text based on the input it receives, allowing it to engage in discussions and provide explanations and descriptions on a wide range of topics.\n\nOverall, Assistant is a powerful system that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether you need help with a specific question or just want to have a conversation about a particular topic, Assistant is here to assist. However, above all else, all responses must adhere to the format of RESPONSE FORMAT INSTRUCTIONS.',
additional_kwargs: {},
},
},
{
lc: 1,
type: 'constructor',
id: ['langchain', 'schema', 'HumanMessage'],
kwargs: {
content:
'TOOLS\n------\nAssistant can ask the user to use tools to look up information that may be helpful in answering the users original question. The tools the human can use are:\n\n\n\nRESPONSE FORMAT INSTRUCTIONS\n----------------------------\n\nOutput a JSON markdown code snippet containing a valid JSON object in one of two formats:\n\n**Option 1:**\nUse this if you want the human to use a tool.\nMarkdown code snippet formatted in the following schema:\n\n```json\n{\n "action": string, // The action to take. Must be one of []\n "action_input": string // The input to the action. May be a stringified object.\n}\n```\n\n**Option #2:**\nUse this if you want to respond directly and conversationally to the human. Markdown code snippet formatted in the following schema:\n\n```json\n{\n "action": "Final Answer",\n "action_input": string // You should put what you want to return to use here and make sure to use valid json newline characters.\n}\n```\n\nFor both options, remember to always include the surrounding markdown code snippet delimiters (begin with "```json" and end with "```")!\n\n\nUSER\'S INPUT\n--------------------\nHere is the user\'s input (remember to respond with a markdown code snippet of a json blob with a single action, and NOTHING else):\n\nHello!',
additional_kwargs: {},
},
},
],
options: { stop: ['Observation:'], promptIndex: 0 },
},
},
],
],
},
}),
createMockNodeExecutionData(AGENT_NODE_NAME, {
jsonData: {
main: { output: 'Hi there! How can I assist you today?' },
},
metadata: {
subRun: [{ node: AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME, runIndex: 0 }],
},
}),
],
lastNodeExecuted: AGENT_NODE_NAME,
});
const messages = getManualChatMessages();
messages.should('have.length', 2);
messages.should('contain', inputMessage);
messages.should('contain', outputMessage);
getManualChatModalLogsTree().should('be.visible');
getManualChatModalLogsEntries().should('have.length', 1);
closeManualChatModal();
});
});

View file

@ -0,0 +1,118 @@
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { MainSidebar } from '../pages';
import { INSTANCE_OWNER } from '../constants';
const WorkflowsPage = new WorkflowsPageClass();
const WorkflowPages = new WorkflowPageClass();
const mainSidebar = new MainSidebar();
describe.skip('Workflow filters', () => {
before(() => {
cy.enableFeature('sharing', true);
});
beforeEach(() => {
cy.visit(WorkflowsPage.url);
});
it('Should filter by tags', () => {
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.newWorkflowButtonCard().click();
cy.createFixtureWorkflow('Test_workflow_1.json', `Workflow 1`);
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.createWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_2.json', `Workflow 2`);
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.workflowFilterButton().click();
WorkflowsPage.getters.workflowTagsDropdown().click();
WorkflowsPage.getters.workflowTagItem('other-tag-1').click();
cy.get('body').click(0, 0);
WorkflowsPage.getters.workflowCards().should('have.length', 1);
WorkflowsPage.getters.workflowCard('Workflow 2').should('contain.text', 'Workflow 2');
mainSidebar.actions.goToSettings();
cy.go('back');
WorkflowsPage.getters.workflowCards().should('have.length', 1);
WorkflowsPage.getters.workflowCard('Workflow 2').should('contain.text', 'Workflow 2');
WorkflowsPage.getters.workflowResetFilters().click();
WorkflowsPage.getters.workflowCards().each(($el) => {
const workflowName = $el.find('[data-test-id="workflow-card-name"]').text();
WorkflowsPage.getters.workflowCardActions(workflowName).click();
WorkflowsPage.getters.workflowDeleteButton().click();
cy.get('button').contains('delete').click();
});
});
it('Should filter by status', () => {
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.newWorkflowButtonCard().click();
cy.createFixtureWorkflow('Test_workflow_1.json', `Workflow 1`);
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.createWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_3.json', `Workflow 3`);
WorkflowPages.getters.activatorSwitch().click();
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.workflowFilterButton().click();
WorkflowsPage.getters.workflowStatusDropdown().click();
WorkflowsPage.getters.workflowStatusItem('Active').click();
cy.get('body').click(0, 0);
WorkflowsPage.getters.workflowCards().should('have.length', 1);
WorkflowsPage.getters.workflowCard('Workflow 3').should('contain.text', 'Workflow 3');
mainSidebar.actions.goToSettings();
cy.go('back');
WorkflowsPage.getters.workflowCards().should('have.length', 1);
WorkflowsPage.getters.workflowCard('Workflow 3').should('contain.text', 'Workflow 3');
WorkflowsPage.getters.workflowResetFilters().click();
WorkflowsPage.getters.workflowCards().each(($el) => {
const workflowName = $el.find('[data-test-id="workflow-card-name"]').text();
WorkflowsPage.getters.workflowCardActions(workflowName).click();
WorkflowsPage.getters.workflowDeleteButton().click();
cy.get('button').contains('delete').click();
});
});
it('Should filter by owned by', () => {
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.newWorkflowButtonCard().click();
cy.createFixtureWorkflow('Test_workflow_1.json', `Workflow 1`);
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.createWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_3.json', `Workflow 3`);
WorkflowPages.getters.activatorSwitch().click();
cy.visit(WorkflowsPage.url);
WorkflowsPage.getters.workflowFilterButton().click();
WorkflowsPage.getters.workflowOwnershipDropdown().realClick();
WorkflowsPage.getters.workflowOwner(INSTANCE_OWNER.email).click();
cy.get('body').click(0, 0);
WorkflowsPage.getters.workflowCards().should('have.length', 2);
mainSidebar.actions.goToSettings();
cy.go('back');
WorkflowsPage.getters.workflowResetFilters().click();
WorkflowsPage.getters.workflowCards().each(($el) => {
const workflowName = $el.find('[data-test-id="workflow-card-name"]').text();
WorkflowsPage.getters.workflowCardActions(workflowName).click();
WorkflowsPage.getters.workflowDeleteButton().click();
cy.get('button').contains('delete').click();
});
});
});

23
cypress/e2e/31-demo.cy.ts Normal file
View file

@ -0,0 +1,23 @@
import workflow from '../fixtures/Manual_wait_set.json';
import { importWorkflow, vistDemoPage } from '../pages/demo';
import { WorkflowPage } from '../pages/workflow';
const workflowPage = new WorkflowPage();
describe('Demo', () => {
it('can import template', () => {
vistDemoPage();
importWorkflow(workflow);
workflowPage.getters.canvasNodes().should('have.length', 3);
});
it('can override theme to dark', () => {
vistDemoPage('dark');
cy.get('body').should('have.attr', 'data-theme', 'dark');
});
it('can override theme to light', () => {
vistDemoPage('light');
cy.get('body').should('have.attr', 'data-theme', 'light');
});
});

View file

@ -0,0 +1,116 @@
import { WorkflowPage as WorkflowPageClass, NDV } from '../pages';
const workflowPage = new WorkflowPageClass();
const ndv = new NDV();
describe('Node IO Filter', () => {
beforeEach(() => {
workflowPage.actions.visit();
cy.createFixtureWorkflow('Node_IO_filter.json', `Node IO filter`);
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.executeWorkflow();
});
it('should filter pinned data', () => {
workflowPage.getters.canvasNodes().first().dblclick();
ndv.actions.close();
workflowPage.getters.canvasNodes().first().dblclick();
cy.wait(500);
ndv.getters.outputDataContainer().should('be.visible');
cy.document().trigger('keyup', { key: '/' });
const searchInput = ndv.getters.searchInput();
searchInput.filter(':focus').should('exist');
ndv.getters.pagination().find('li').should('have.length', 3);
cy.get('.highlight').should('not.exist');
searchInput.type('ar');
ndv.getters.pagination().find('li').should('have.length', 2);
cy.get('.highlight').its('length').should('be.gt', 0);
searchInput.type('i');
ndv.getters.pagination().should('not.exist');
cy.get('.highlight').its('length').should('be.gt', 0);
});
it.only('should filter input/output data separately', () => {
workflowPage.getters.canvasNodes().eq(1).dblclick();
cy.wait(500);
ndv.getters.outputDataContainer().should('be.visible');
ndv.getters.inputDataContainer().should('be.visible');
ndv.actions.switchInputMode('Table');
cy.document().trigger('keyup', { key: '/' });
ndv.getters.outputPanel().findChildByTestId('ndv-search').filter(':focus').should('not.exist');
let focusedInput = ndv.getters
.inputPanel()
.findChildByTestId('ndv-search')
.filter(':focus')
.should('exist');
const getInputPagination = () =>
ndv.getters.inputPanel().findChildByTestId('ndv-data-pagination');
const getInputCounter = () => ndv.getters.inputPanel().findChildByTestId('ndv-items-count');
const getOuputPagination = () =>
ndv.getters.outputPanel().findChildByTestId('ndv-data-pagination');
const getOutputCounter = () => ndv.getters.outputPanel().findChildByTestId('ndv-items-count');
getInputPagination().find('li').should('have.length', 3);
getInputCounter().contains('21 items').should('exist');
getOuputPagination().find('li').should('have.length', 3);
getOutputCounter().contains('21 items').should('exist');
focusedInput.type('ar');
getInputPagination().find('li').should('have.length', 2);
getInputCounter().should('contain', '14 of 21 items');
getOuputPagination().find('li').should('have.length', 3);
getOutputCounter().should('contain', '21 items');
focusedInput.type('i');
getInputPagination().should('not.exist');
getInputCounter().should('contain', '8 of 21 items');
getOuputPagination().find('li').should('have.length', 3);
getOutputCounter().should('contain', '21 items');
focusedInput.clear();
getInputPagination().find('li').should('have.length', 3);
getInputCounter().contains('21 items').should('exist');
getOuputPagination().find('li').should('have.length', 3);
getOutputCounter().contains('21 items').should('exist');
ndv.getters.outputDataContainer().trigger('mouseover');
cy.document().trigger('keyup', { key: '/' });
ndv.getters.inputPanel().findChildByTestId('ndv-search').filter(':focus').should('not.exist');
focusedInput = ndv.getters
.outputPanel()
.findChildByTestId('ndv-search')
.filter(':focus')
.should('exist');
getInputPagination().find('li').should('have.length', 3);
getInputCounter().contains('21 items').should('exist');
getOuputPagination().find('li').should('have.length', 3);
getOutputCounter().contains('21 items').should('exist');
focusedInput.type('ar');
getInputPagination().find('li').should('have.length', 3);
getInputCounter().contains('21 items').should('exist');
getOuputPagination().find('li').should('have.length', 2);
getOutputCounter().should('contain', '14 of 21 items');
focusedInput.type('i');
getInputPagination().find('li').should('have.length', 3);
getInputCounter().contains('21 items').should('exist');
getOuputPagination().should('not.exist');
getOutputCounter().should('contain', '8 of 21 items');
focusedInput.clear();
getInputPagination().find('li').should('have.length', 3);
getInputCounter().contains('21 items').should('exist');
getOuputPagination().find('li').should('have.length', 3);
getOutputCounter().contains('21 items').should('exist');
});
});

View file

@ -0,0 +1,43 @@
import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants';
import { WorkerViewPage } from '../pages';
const workerViewPage = new WorkerViewPage();
describe('Worker View (unlicensed)', () => {
beforeEach(() => {
cy.disableFeature('workerView');
cy.disableQueueMode();
});
it('should not show up in the menu sidebar', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.visit(workerViewPage.url);
workerViewPage.getters.menuItem().should('not.exist');
});
it('should show action box', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.visit(workerViewPage.url);
workerViewPage.getters.workerViewUnlicensed().should('exist');
});
});
describe('Worker View (licensed)', () => {
beforeEach(() => {
cy.enableFeature('workerView');
cy.enableQueueMode();
});
it('should show up in the menu sidebar', () => {
cy.signin(INSTANCE_OWNER);
cy.enableQueueMode();
cy.visit(workerViewPage.url);
workerViewPage.getters.menuItem().should('exist');
});
it('should show worker list view', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.visit(workerViewPage.url);
workerViewPage.getters.workerViewLicensed().should('exist');
});
});

View file

@ -0,0 +1,52 @@
import { WorkflowPage } from "../pages";
const workflowPage = new WorkflowPage();
const INVALID_NAMES = [
'https://n8n.io',
'http://n8n.io',
'www.n8n.io',
'n8n.io',
'n8n.бг',
'n8n.io/home',
'n8n.io/home?send=true',
'<a href="#">Jack</a>',
'<script>alert("Hello")</script>',
];
const VALID_NAMES = [
['a', 'a'],
['alice', 'alice'],
['Robert', 'Downey Jr.'],
['Mia', 'Mia-Downey'],
['Mark', "O'neil"],
['Thomas', 'Müler'],
['ßáçøñ', 'ßáçøñ'],
['أحمد', 'فلسطين'],
['Милорад', 'Филиповић'],
];
describe('Personal Settings', () => {
it ('should allow to change first and last name', () => {
cy.visit('/settings/personal');
VALID_NAMES.forEach((name) => {
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name[0]);
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name[1]);
cy.getByTestId('save-settings-button').click();
workflowPage.getters.successToast().should('contain', 'Personal details updated');
workflowPage.getters.successToast().find('.el-notification__closeBtn').click();
});
});
it('not allow malicious values for personal data', () => {
cy.visit('/settings/personal');
INVALID_NAMES.forEach((name) => {
cy.getByTestId('personal-data-form').find('input[name="firstName"]').clear().type(name);
cy.getByTestId('personal-data-form').find('input[name="lastName"]').clear().type(name);
cy.getByTestId('save-settings-button').click();
workflowPage.getters
.errorToast()
.should('contain', 'Malicious firstName | Malicious lastName');
workflowPage.getters.errorToast().find('.el-notification__closeBtn').click();
});
});
});

View file

@ -0,0 +1,212 @@
import {
clickUseWorkflowButtonByTitle,
visitTemplateCollectionPage,
testData,
} from '../pages/template-collection';
import * as templateCredentialsSetupPage from '../pages/template-credential-setup';
import { TemplateWorkflowPage } from '../pages/template-workflow';
import { WorkflowPage } from '../pages/workflow';
import * as formStep from '../composables/setup-template-form-step';
import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button';
import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal';
const templateWorkflowPage = new TemplateWorkflowPage();
const workflowPage = new WorkflowPage();
const testTemplate = templateCredentialsSetupPage.testData.simpleTemplate;
// NodeView uses beforeunload listener that will show a browser
// native popup, which will block cypress from continuing / exiting.
// This prevent the registration of the listener.
Cypress.on('window:before:load', (win) => {
const origAddEventListener = win.addEventListener;
win.addEventListener = (eventName: string, listener: any, opts: any) => {
if (eventName === 'beforeunload') {
return;
}
return origAddEventListener.call(win, eventName, listener, opts);
};
});
describe('Template credentials setup', () => {
beforeEach(() => {
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${testTemplate.id}`, {
fixture: testTemplate.fixture,
});
});
it('can be opened from template workflow page', () => {
templateWorkflowPage.actions.visit(testTemplate.id);
templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag();
templateWorkflowPage.getters.useTemplateButton().should('be.visible');
templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag();
templateWorkflowPage.actions.clickUseThisWorkflowButton();
templateCredentialsSetupPage.getters
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
.should('be.visible');
});
it('can be opened from template collection page', () => {
visitTemplateCollectionPage(testData.ecommerceStarterPack);
templateCredentialsSetupPage.enableTemplateCredentialSetupFeatureFlag();
clickUseWorkflowButtonByTitle('Promote new Shopify products on Twitter and Telegram');
templateCredentialsSetupPage.getters
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
.should('be.visible');
});
it('has all the elements on page', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.getters
.title(`Set up 'Promote new Shopify products on Twitter and Telegram' template`)
.should('be.visible');
templateCredentialsSetupPage.getters
.infoCallout()
.should(
'contain.text',
'You need 1x Shopify, 1x X (Formerly Twitter) and 1x Telegram account to setup this template',
);
const expectedAppNames = ['1. Shopify', '2. X (Formerly Twitter)', '3. Telegram'];
const expectedAppDescriptions = [
'The credential you select will be used in the product created node of the workflow template.',
'The credential you select will be used in the Twitter node of the workflow template.',
'The credential you select will be used in the Telegram node of the workflow template.',
];
formStep.getFormStep().each(($el, index) => {
formStep.getStepHeading($el).should('have.text', expectedAppNames[index]);
formStep.getStepDescription($el).should('have.text', expectedAppDescriptions[index]);
});
});
it('can skip template creation', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.getters.skipLink().click();
workflowPage.getters.canvasNodes().should('have.length', 3);
});
it('can create credentials and workflow from the template', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
// Continue button should be disabled if no credentials are created
templateCredentialsSetupPage.getters.continueButton().should('be.disabled');
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Shopify');
// Continue button should be enabled if at least one has been created
templateCredentialsSetupPage.getters.continueButton().should('be.enabled');
templateCredentialsSetupPage.fillInDummyCredentialsForAppWithConfirm('X (Formerly Twitter)');
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Telegram');
templateCredentialsSetupPage.finishCredentialSetup();
workflowPage.getters.canvasNodes().should('have.length', 3);
// Focus the canvas so the copy to clipboard works
workflowPage.getters.canvasNodes().eq(0).realClick();
workflowPage.actions.selectAll();
workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
// Check workflow JSON by copying it to clipboard
cy.readClipboard().then((workflowJSON) => {
const workflow = JSON.parse(workflowJSON);
expect(workflow.meta).to.haveOwnProperty('templateId', testTemplate.id.toString());
workflow.nodes.forEach((node: any) => {
expect(Object.keys(node.credentials ?? {})).to.have.lengthOf(1);
});
});
});
it('should work with a template that has no credentials (ADO-1603)', () => {
const templateWithoutCreds = templateCredentialsSetupPage.testData.templateWithoutCredentials;
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${templateWithoutCreds.id}`, {
fixture: templateWithoutCreds.fixture,
});
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(templateWithoutCreds.id);
const expectedAppNames = ['1. Email (IMAP)', '2. Nextcloud'];
const expectedAppDescriptions = [
'The credential you select will be used in the IMAP Email node of the workflow template.',
'The credential you select will be used in the Nextcloud node of the workflow template.',
];
formStep.getFormStep().each(($el, index) => {
formStep.getStepHeading($el).should('have.text', expectedAppNames[index]);
formStep.getStepDescription($el).should('have.text', expectedAppDescriptions[index]);
});
templateCredentialsSetupPage.getters.continueButton().should('be.disabled');
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Email (IMAP)');
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Nextcloud');
templateCredentialsSetupPage.finishCredentialSetup();
workflowPage.getters.canvasNodes().should('have.length', 3);
});
describe('Credential setup from workflow editor', () => {
beforeEach(() => {
cy.resetDatabase();
cy.signinAsOwner();
});
it('should allow credential setup from workflow editor if user skips it during template setup', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.getters.skipLink().click();
getSetupWorkflowCredentialsButton().should('be.visible');
});
it('should allow credential setup from workflow editor if user fills in credentials partially during template setup', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Shopify');
templateCredentialsSetupPage.finishCredentialSetup();
getSetupWorkflowCredentialsButton().should('be.visible');
});
it('should fill credentials from workflow editor', () => {
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(testTemplate.id);
templateCredentialsSetupPage.getters.skipLink().click();
getSetupWorkflowCredentialsButton().click();
setupCredsModal.getWorkflowCredentialsModal().should('be.visible');
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Shopify');
templateCredentialsSetupPage.fillInDummyCredentialsForAppWithConfirm('X (Formerly Twitter)');
templateCredentialsSetupPage.fillInDummyCredentialsForApp('Telegram');
setupCredsModal.closeModalFromContinueButton();
setupCredsModal.getWorkflowCredentialsModal().should('not.exist');
// Focus the canvas so the copy to clipboard works
workflowPage.getters.canvasNodes().eq(0).realClick();
workflowPage.actions.selectAll();
workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
// Check workflow JSON by copying it to clipboard
cy.readClipboard().then((workflowJSON) => {
const workflow = JSON.parse(workflowJSON);
workflow.nodes.forEach((node: any) => {
expect(Object.keys(node.credentials ?? {})).to.have.lengthOf(1);
});
});
getSetupWorkflowCredentialsButton().should('not.exist');
});
});
});

View file

@ -0,0 +1,23 @@
import { INSTANCE_ADMIN, INSTANCE_OWNER } from '../constants';
import { SettingsPage } from '../pages/settings';
const settingsPage = new SettingsPage();
describe('Admin user', { disableAutoLogin: true }, () => {
it('should see same Settings sub menu items as instance owner', () => {
cy.signin(INSTANCE_OWNER);
cy.visit(settingsPage.url);
let ownerMenuItems = 0;
settingsPage.getters.menuItems().then(($el) => {
ownerMenuItems = $el.length;
});
cy.signout();
cy.signin(INSTANCE_ADMIN);
cy.visit(settingsPage.url);
settingsPage.getters.menuItems().should('have.length', ownerMenuItems);
});
});

View file

@ -0,0 +1,143 @@
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
type SuggestedTemplatesStub = {
sections: SuggestedTemplatesSectionStub[];
}
type SuggestedTemplatesSectionStub = {
name: string;
title: string;
description: string;
workflows: Array<Object>;
};
const WorkflowsListPage = new WorkflowsPageClass();
const WorkflowPage = new WorkflowPageClass();
let fixtureSections: SuggestedTemplatesStub = { sections: [] };;
describe('Suggested templates - Should render', () => {
before(() => {
cy.fixture('Suggested_Templates.json').then((data) => {
fixtureSections = data;
});
});
beforeEach(() => {
localStorage.removeItem('SHOW_N8N_SUGGESTED_TEMPLATES');
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' } },
});
});
}).as('loadSettings');
cy.intercept('GET', '/rest/cloud/proxy/templates', {
fixture: 'Suggested_Templates.json',
});
cy.visit(WorkflowsListPage.url);
cy.wait('@loadSettings');
});
it('should render suggested templates page in empty workflow list', () => {
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('exist');
WorkflowsListPage.getters.suggestedTemplatesCards().should('have.length', fixtureSections.sections[0].workflows.length);
WorkflowsListPage.getters.suggestedTemplatesSectionDescription().should('contain', fixtureSections.sections[0].description);
});
it('should render suggested templates when there are workflows in the list', () => {
WorkflowsListPage.getters.suggestedTemplatesNewWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_1.json', 'Test Workflow');
cy.visit(WorkflowsListPage.url);
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('exist');
cy.contains(`Explore ${fixtureSections.sections[0].name.toLocaleLowerCase()} workflow templates`).should('exist');
WorkflowsListPage.getters.suggestedTemplatesCards().should('have.length', fixtureSections.sections[0].workflows.length);
});
it('should enable users to signup for suggested templates templates', () => {
// Test the whole flow
WorkflowsListPage.getters.suggestedTemplatesCards().first().click();
WorkflowsListPage.getters.suggestedTemplatesPreviewModal().should('exist');
WorkflowsListPage.getters.suggestedTemplatesUseTemplateButton().click();
cy.url().should('include', '/workflow/new');
WorkflowPage.getters.infoToast().should('contain', 'Template coming soon!');
WorkflowPage.getters.infoToast().contains('Notify me when it\'s available').click();
WorkflowPage.getters.successToast().should('contain', 'We will contact you via email once this template is released.');
cy.visit(WorkflowsListPage.url);
// Once users have signed up for a template, suggestions should not be shown again
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
});
});
describe('Suggested templates - Should not render', () => {
beforeEach(() => {
localStorage.removeItem('SHOW_N8N_SUGGESTED_TEMPLATES');
cy.visit(WorkflowsListPage.url);
});
it('should not render suggested templates templates if not in cloud deployment', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'notCloud' } },
});
});
});
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
});
it('should not render suggested templates templates if endpoint throws error', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' } },
});
});
});
cy.intercept('GET', '/rest/cloud/proxy/templates', { statusCode: 500 }).as('loadTemplates');
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
});
it('should not render suggested templates templates if endpoint returns empty list', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' } },
});
});
});
cy.intercept('GET', '/rest/cloud/proxy/templates', (req) => {
req.on('response', (res) => {
res.send({
data: { collections: [] },
});
});
});
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
});
it('should not render suggested templates templates if endpoint returns invalid response', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: { ...res.body.data, deployment: { type: 'cloud' } },
});
});
});
cy.intercept('GET', '/rest/cloud/proxy/templates', (req) => {
req.on('response', (res) => {
res.send({
data: { somethingElse: [] },
});
});
});
WorkflowsListPage.getters.suggestedTemplatesPageContainer().should('not.exist');
WorkflowsListPage.getters.suggestedTemplatesSectionContainer().should('not.exist');
});
});

View file

@ -0,0 +1,66 @@
import { INSTANCE_OWNER } from '../constants';
import { WorkflowsPage } from '../pages/workflows';
import {
closeVersionUpdatesPanel,
getVersionCard,
getVersionUpdatesPanelOpenButton,
openVersionUpdatesPanel,
} from '../composables/versions';
const workflowsPage = new WorkflowsPage();
describe('Versions', () => {
it('should open updates panel', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.continue((res) => {
if (res.body.hasOwnProperty('data')) {
res.body.data = {
...res.body.data,
releaseChannel: 'stable',
versionCli: '1.0.0',
versionNotifications: {
enabled: true,
endpoint: 'https://api.n8n.io/api/versions/',
infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html',
},
};
}
});
}).as('settings');
cy.intercept('GET', 'https://api.n8n.io/api/versions/1.0.0', [
{
name: '1.3.1',
createdAt: '2023-08-18T11:53:12.857Z',
hasSecurityIssue: null,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: null,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n131',
nodes: [],
description: 'Includes <strong>bug fixes</strong>',
},
{
name: '1.0.5',
createdAt: '2023-07-24T10:54:56.097Z',
hasSecurityIssue: false,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: true,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n104',
nodes: [],
description: 'Includes <strong>core functionality</strong> and <strong>bug fixes</strong>',
},
]);
cy.signin(INSTANCE_OWNER);
cy.visit(workflowsPage.url);
cy.wait('@settings');
getVersionUpdatesPanelOpenButton().should('contain', '2 updates');
openVersionUpdatesPanel();
getVersionCard().should('have.length', 2);
closeVersionUpdatesPanel();
});
});

View file

@ -2,6 +2,7 @@ import { NodeCreator } from '../pages/features/node-creator';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { NDV } from '../pages/ndv'; import { NDV } from '../pages/ndv';
import { getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
import { IF_NODE_NAME } from '../constants';
const nodeCreatorFeature = new NodeCreator(); const nodeCreatorFeature = new NodeCreator();
const WorkflowPage = new WorkflowPageClass(); const WorkflowPage = new WorkflowPageClass();
@ -34,7 +35,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.actions.openNodeCreator(); nodeCreatorFeature.actions.openNodeCreator();
nodeCreatorFeature.getters.searchBar().find('input').type('manual'); nodeCreatorFeature.getters.searchBar().find('input').type('manual');
nodeCreatorFeature.getters.creatorItem().should('have.length', 1); nodeCreatorFeature.getters.creatorItem().should('have.length', 2);
nodeCreatorFeature.getters.searchBar().find('input').clear().type('manual123'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('manual123');
nodeCreatorFeature.getters.creatorItem().should('have.length', 0); nodeCreatorFeature.getters.creatorItem().should('have.length', 0);
nodeCreatorFeature.getters nodeCreatorFeature.getters
@ -101,15 +102,15 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.searchBar().find('input').type('{rightarrow}'); nodeCreatorFeature.getters.searchBar().find('input').type('{rightarrow}');
nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'FTP'); nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'FTP');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('file'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('file');
// Navigate to rename action which should be the 4th item // The 1st trigger is selected, up 1x to the collapsable header, up 2x to the last action (rename)
nodeCreatorFeature.getters.searchBar().find('input').type('{uparrow}{rightarrow}'); nodeCreatorFeature.getters.searchBar().find('input').type('{uparrow}{uparrow}{rightarrow}');
NDVModal.getters.parameterInput('operation').find('input').should('have.value', 'Rename'); NDVModal.getters.parameterInput('operation').find('input').should('have.value', 'Rename');
}); });
it('should not show actions for single action nodes', () => { it('should not show actions for single action nodes', () => {
const singleActionNodes = [ const singleActionNodes = [
'DHL', 'DHL',
'iCalendar', 'Edit Fields',
'LingvaNex', 'LingvaNex',
'Mailcheck', 'Mailcheck',
'MSG91', 'MSG91',
@ -307,7 +308,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCategoryItem('Actions').click();
nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click();
NDVModal.actions.close(); NDVModal.actions.close();
WorkflowPage.actions.deleteNode('When clicking "Execute Workflow"'); WorkflowPage.actions.deleteNode('When clicking "Test Workflow"');
WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click(); WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click();
nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n');
nodeCreatorFeature.getters.getCreatorItem('n8n').click(); nodeCreatorFeature.getters.getCreatorItem('n8n').click();
@ -316,7 +317,7 @@ describe('Node Creator', () => {
NDVModal.actions.close(); NDVModal.actions.close();
WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.canvasNodes().should('have.length', 2);
WorkflowPage.actions.zoomToFit(); WorkflowPage.actions.zoomToFit();
WorkflowPage.actions.addNodeBetweenNodes('n8n', 'n8n1', 'Item Lists', 'Summarize'); WorkflowPage.actions.addNodeBetweenNodes('n8n', 'n8n1', 'Summarize');
WorkflowPage.getters.canvasNodes().should('have.length', 3); WorkflowPage.getters.canvasNodes().should('have.length', 3);
}); });
}); });
@ -360,7 +361,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('i'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('i');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'IF'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME);
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch'); nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw');
@ -368,11 +369,11 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('i'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('i');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'IF'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME);
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch'); nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('IF'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('IF');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'IF'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME);
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch'); nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw');
@ -410,7 +411,7 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.searchBar().find('input').clear().type('js'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('js');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Code'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Code');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Item Lists'); nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Edit Fields (Set)');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('fi'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('fi');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Filter'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Filter');
@ -478,15 +479,14 @@ describe('Node Creator', () => {
nodeCreatorFeature.getters.searchBar().find('input').clear().type('wa'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('wa');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Wait'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Wait');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Merge');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('wait'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('wait');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Wait'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Wait');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Merge');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('spreadsheet'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('spreadsheet');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Spreadsheet File'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Convert to File');
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Google Sheets'); nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Extract From File');
nodeCreatorFeature.getters.nodeItemName().eq(2).should('have.text', 'Google Sheets');
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sheets'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('sheets');
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Google Sheets'); nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Google Sheets');

View file

@ -1,7 +1,7 @@
import { WorkflowPage, NDV } from '../pages';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { getPopper, getVisiblePopper, getVisibleSelect } from '../utils'; import { getVisibleSelect } from '../utils';
import { META_KEY } from '../constants'; import { MANUAL_TRIGGER_NODE_DISPLAY_NAME } from '../constants';
import { NDV, WorkflowPage } from '../pages';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const ndv = new NDV(); const ndv = new NDV();
@ -42,7 +42,7 @@ describe('NDV', () => {
ndv.getters.outputDisplayMode().should('have.length.at.least', 1).and('be.visible'); ndv.getters.outputDisplayMode().should('have.length.at.least', 1).and('be.visible');
}); });
it('should change input', () => { it('should change input and go back to canvas', () => {
cy.createFixtureWorkflow('NDV-test-select-input.json', `NDV test select input ${uuid()}`); cy.createFixtureWorkflow('NDV-test-select-input.json', `NDV test select input ${uuid()}`);
workflowPage.actions.zoomToFit(); workflowPage.actions.zoomToFit();
workflowPage.getters.canvasNodes().last().dblclick(); workflowPage.getters.canvasNodes().last().dblclick();
@ -50,6 +50,9 @@ describe('NDV', () => {
ndv.getters.inputOption().last().click(); ndv.getters.inputOption().last().click();
ndv.getters.inputDataContainer().find('[class*=schema_]').should('exist'); ndv.getters.inputDataContainer().find('[class*=schema_]').should('exist');
ndv.getters.inputDataContainer().should('contain', 'start'); ndv.getters.inputDataContainer().should('contain', 'start');
ndv.getters.backToCanvas().click();
ndv.getters.container().should('not.be.visible');
cy.shouldNotHaveConsoleErrors();
}); });
it('should show correct validation state for resource locator params', () => { it('should show correct validation state for resource locator params', () => {
@ -68,10 +71,10 @@ describe('NDV', () => {
workflowPage.actions.addNodeToCanvas('Manual'); workflowPage.actions.addNodeToCanvas('Manual');
workflowPage.actions.addNodeToCanvas('Airtable', true, true, 'Search records'); workflowPage.actions.addNodeToCanvas('Airtable', true, true, 'Search records');
ndv.getters.container().should('be.visible'); ndv.getters.container().should('be.visible');
// cy.get('.has-issues').should('have.length', 0); cy.get('.has-issues').should('have.length', 0);
ndv.getters.parameterInput('table').find('input').eq(1).focus().blur(); ndv.getters.parameterInput('table').find('input').eq(1).focus().blur();
ndv.getters.parameterInput('base').find('input').eq(1).focus().blur(); ndv.getters.parameterInput('base').find('input').eq(1).focus().blur();
cy.get('.has-issues').should('have.length', 0); cy.get('.has-issues').should('have.length', 2);
ndv.getters.backToCanvas().click(); ndv.getters.backToCanvas().click();
workflowPage.actions.openNode('Airtable'); workflowPage.actions.openNode('Airtable');
cy.get('.has-issues').should('have.length', 2); cy.get('.has-issues').should('have.length', 2);
@ -299,11 +302,11 @@ describe('NDV', () => {
ndv.actions.setInvalidExpression({ fieldName: 'fieldId', delay: 200 }); ndv.actions.setInvalidExpression({ fieldName: 'fieldId', delay: 200 });
ndv.getters.container().click(); // remove focus from input, hide expression preview ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview
ndv.getters.parameterInput('remoteOptions').click(); ndv.getters.parameterInput('remoteOptions').click();
ndv.getters.parameterInputIssues('remoteOptions').realHover(); ndv.getters.parameterInputIssues('remoteOptions').realHover({ scrollBehavior: false});
// Remote options dropdown should not be visible // Remote options dropdown should not be visible
ndv.getters.parameterInput('remoteOptions').find('.el-select').should('not.exist'); ndv.getters.parameterInput('remoteOptions').find('.el-select').should('not.exist');
}); });
@ -317,7 +320,7 @@ describe('NDV', () => {
ndv.actions.setInvalidExpression({ fieldName: 'otherField', delay: 50 }); ndv.actions.setInvalidExpression({ fieldName: 'otherField', delay: 50 });
ndv.getters.container().click(); // remove focus from input, hide expression preview ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview
ndv.getters.parameterInput('remoteOptions').click(); ndv.getters.parameterInput('remoteOptions').click();
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3); getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
@ -357,12 +360,253 @@ describe('NDV', () => {
ndv.getters.nodeExecuteButton().should('be.visible'); ndv.getters.nodeExecuteButton().should('be.visible');
}); });
it('should allow editing code in fullscreen in the Code node', () => {
workflowPage.actions.addInitialNodeToCanvas('Code', { keepNdvOpen: true });
ndv.actions.openCodeEditorFullscreen();
ndv.getters.codeEditorFullscreen().type('{selectall}').type('{backspace}').type('foo()');
ndv.getters.codeEditorFullscreen().should('contain.text', 'foo()');
cy.wait(200);
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
ndv.getters.parameterInput('jsCode').get('.cm-content').should('contain.text', 'foo()');
});
it('should not retrieve remote options when a parameter value changes', () => { it('should not retrieve remote options when a parameter value changes', () => {
cy.intercept('/rest/node-parameter-options?**', cy.spy().as('fetchParameterOptions')); cy.intercept('/rest/dynamic-node-parameters/options?**', cy.spy().as('fetchParameterOptions'));
workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' }); workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' });
// Type something into the field // Type something into the field
ndv.actions.typeIntoParameterInput('otherField', 'test'); ndv.actions.typeIntoParameterInput('otherField', 'test');
// Should call the endpoint only once (on mount), not for every keystroke // Should call the endpoint only once (on mount), not for every keystroke
cy.get('@fetchParameterOptions').should('have.been.calledOnce'); cy.get('@fetchParameterOptions').should('have.been.calledOnce');
}); });
describe('floating nodes', () => {
function getFloatingNodeByPosition(
position: 'inputMain' | 'outputMain' | 'outputSub' | 'inputSub',
) {
return cy.get(`[data-node-placement=${position}]`);
}
beforeEach(() => {
cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`);
workflowPage.getters.canvasNodes().first().dblclick();
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('exist');
});
it('should traverse floating nodes with mouse', () => {
// Traverse 4 connected node forwards
Array.from(Array(4).keys()).forEach((i) => {
getFloatingNodeByPosition('outputMain').click({ force: true });
ndv.getters.nodeNameContainer().should('contain', `Node ${i + 1}`);
getFloatingNodeByPosition('inputMain').should('exist');
getFloatingNodeByPosition('outputMain').should('exist');
ndv.actions.close();
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters
.selectedNodes()
.first()
.should('contain', `Node ${i + 1}`);
workflowPage.getters.selectedNodes().first().dblclick();
});
getFloatingNodeByPosition('outputMain').click({ force: true });
ndv.getters.nodeNameContainer().should('contain', 'Chain');
getFloatingNodeByPosition('inputSub').should('exist');
getFloatingNodeByPosition('inputSub').click({ force: true });
ndv.getters.nodeNameContainer().should('contain', 'Model');
getFloatingNodeByPosition('inputSub').should('not.exist');
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('not.exist');
getFloatingNodeByPosition('outputSub').should('exist');
ndv.actions.close();
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters.selectedNodes().first().should('contain', 'Model');
workflowPage.getters.selectedNodes().first().dblclick();
getFloatingNodeByPosition('outputSub').click({ force: true });
ndv.getters.nodeNameContainer().should('contain', 'Chain');
// Traverse 4 connected node backwards
Array.from(Array(4).keys()).forEach((i) => {
getFloatingNodeByPosition('inputMain').click({ force: true });
ndv.getters.nodeNameContainer().should('contain', `Node ${4 - i}`);
getFloatingNodeByPosition('outputMain').should('exist');
getFloatingNodeByPosition('inputMain').should('exist');
});
getFloatingNodeByPosition('inputMain').click({ force: true });
workflowPage.getters
.selectedNodes()
.first()
.should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('inputSub').should('not.exist');
getFloatingNodeByPosition('outputSub').should('not.exist');
ndv.actions.close();
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters
.selectedNodes()
.first()
.should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
});
it('should traverse floating nodes with mouse', () => {
// Traverse 4 connected node forwards
Array.from(Array(4).keys()).forEach((i) => {
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight']);
ndv.getters.nodeNameContainer().should('contain', `Node ${i + 1}`);
getFloatingNodeByPosition('inputMain').should('exist');
getFloatingNodeByPosition('outputMain').should('exist');
ndv.actions.close();
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters
.selectedNodes()
.first()
.should('contain', `Node ${i + 1}`);
workflowPage.getters.selectedNodes().first().dblclick();
});
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight']);
ndv.getters.nodeNameContainer().should('contain', 'Chain');
getFloatingNodeByPosition('inputSub').should('exist');
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowDown']);
ndv.getters.nodeNameContainer().should('contain', 'Model');
getFloatingNodeByPosition('inputSub').should('not.exist');
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('outputMain').should('not.exist');
getFloatingNodeByPosition('outputSub').should('exist');
ndv.actions.close();
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters.selectedNodes().first().should('contain', 'Model');
workflowPage.getters.selectedNodes().first().dblclick();
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowUp']);
ndv.getters.nodeNameContainer().should('contain', 'Chain');
// Traverse 4 connected node backwards
Array.from(Array(4).keys()).forEach((i) => {
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowLeft']);
ndv.getters.nodeNameContainer().should('contain', `Node ${4 - i}`);
getFloatingNodeByPosition('outputMain').should('exist');
getFloatingNodeByPosition('inputMain').should('exist');
});
cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowLeft']);
workflowPage.getters
.selectedNodes()
.first()
.should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
getFloatingNodeByPosition('inputMain').should('not.exist');
getFloatingNodeByPosition('inputSub').should('not.exist');
getFloatingNodeByPosition('outputSub').should('not.exist');
ndv.actions.close();
workflowPage.getters.selectedNodes().should('have.length', 1);
workflowPage.getters
.selectedNodes()
.first()
.should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME);
});
});
it('should show node name and version in settings', () => {
cy.createFixtureWorkflow('Test_workflow_ndv_version.json', `NDV test version ${uuid()}`);
workflowPage.actions.openNode('Edit Fields (old)');
ndv.actions.openSettings();
ndv.getters.nodeVersion().should('have.text', 'Set node version 2 (Latest version: 3.2)');
ndv.actions.close();
workflowPage.actions.openNode('Edit Fields (latest)');
ndv.actions.openSettings();
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.2 (Latest)');
ndv.actions.close();
workflowPage.actions.openNode('Function');
ndv.actions.openSettings();
ndv.getters.nodeVersion().should('have.text', 'Function node version 1 (Deprecated)');
ndv.actions.close();
});
it('Should render xml and html tags as strings and can search', () => {
cy.createFixtureWorkflow('Test_workflow_xml_output.json', `test`);
workflowPage.actions.executeWorkflow();
workflowPage.actions.openNode('Edit Fields');
ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Table');
ndv.getters
.outputTableRow(1)
.should('include.text', '<?xml version="1.0" encoding="UTF-8"?> <library>');
cy.document().trigger('keyup', { key: '/' });
ndv.getters.searchInput().filter(':focus').type('<lib');
ndv.getters.outputTableRow(1).find('mark').should('have.text', '<lib');
ndv.getters.outputDisplayMode().find('label').eq(1).should('include.text', 'JSON');
ndv.getters.outputDisplayMode().find('label').eq(1).click();
ndv.getters.outputDataContainer().find('.json-data').should('exist');
ndv.getters
.outputDataContainer()
.should(
'have.text',
'[{"body": "<?xml version="1.0" encoding="UTF-8"?> <library> <book> <title>Introduction to XML</title> <author>John Doe</author> <publication_year>2020</publication_year> <isbn>1234567890</isbn> </book> <book> <title>Data Science Basics</title> <author>Jane Smith</author> <publication_year>2019</publication_year> <isbn>0987654321</isbn> </book> <book> <title>Programming in Python</title> <author>Bob Johnson</author> <publication_year>2021</publication_year> <isbn>5432109876</isbn> </book> </library>"}]',
);
ndv.getters.outputDataContainer().find('mark').should('have.text', '<lib');
ndv.getters.outputDisplayMode().find('label').eq(2).should('include.text', 'Schema');
ndv.getters.outputDisplayMode().find('label').eq(2).click({ force: true });
ndv.getters
.outputDataContainer()
.findChildByTestId('run-data-schema-item')
.find('> span')
.should('include.text', '<?xml version="1.0" encoding="UTF-8"?>');
});
it('should properly show node execution indicator', () => {
workflowPage.actions.addInitialNodeToCanvas('Code');
workflowPage.actions.openNode('Code');
// Should not show run info before execution
ndv.getters.nodeRunSuccessIndicator().should('not.exist');
ndv.getters.nodeRunErrorIndicator().should('not.exist');
ndv.getters.nodeExecuteButton().click();
ndv.getters.nodeRunSuccessIndicator().should('exist');
});
it('should properly show node execution indicator for multiple nodes', () => {
workflowPage.actions.addInitialNodeToCanvas('Code');
workflowPage.actions.openNode('Code');
ndv.actions.typeIntoParameterInput('jsCode', 'testets');
ndv.getters.backToCanvas().click();
workflowPage.actions.executeWorkflow();
// Manual tigger node should show success indicator
workflowPage.actions.openNode('When clicking "Test Workflow"');
ndv.getters.nodeRunSuccessIndicator().should('exist');
// Code node should show error
ndv.getters.backToCanvas().click();
workflowPage.actions.openNode('Code');
ndv.getters.nodeRunErrorIndicator().should('exist');
});
it('Should handle mismatched option attributes', () => {
workflowPage.actions.addInitialNodeToCanvas('LDAP', {
keepNdvOpen: true,
action: 'Create a new entry',
});
// Add some attributes in Create operation
cy.getByTestId('parameter-item').contains('Add Attributes').click();
ndv.actions.changeNodeOperation('Update');
// Attributes should be empty after operation change
cy.getByTestId('parameter-item').contains('Currently no items exist').should('exist');
});
it('Should keep RLC values after operation change', () => {
const TEST_DOC_ID = '1111';
workflowPage.actions.addInitialNodeToCanvas('Google Sheets', {
keepNdvOpen: true,
action: 'Append row in sheet',
});
ndv.actions.setRLCValue('documentId', TEST_DOC_ID);
ndv.actions.changeNodeOperation('Update Row');
ndv.getters.resourceLocatorInput('documentId').find('input').should('have.value', TEST_DOC_ID);
});
}); });

View file

@ -4,6 +4,8 @@ import {
META_KEY, META_KEY,
SCHEDULE_TRIGGER_NODE_NAME, SCHEDULE_TRIGGER_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME, EDIT_FIELDS_SET_NODE_NAME,
INSTANCE_MEMBERS,
INSTANCE_OWNER,
} from '../constants'; } from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows'; import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows';
@ -100,6 +102,7 @@ describe('Workflow Actions', () => {
cy.get('body').type(META_KEY, { release: false }).type('s'); cy.get('body').type(META_KEY, { release: false }).type('s');
cy.get('body').type(META_KEY, { release: false }).type('s'); cy.get('body').type(META_KEY, { release: false }).type('s');
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(0)); cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(0));
cy.waitForLoad();
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME); WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
cy.get('body').type(META_KEY, { release: false }).type('s'); cy.get('body').type(META_KEY, { release: false }).type('s');
cy.wait('@saveWorkflow'); cy.wait('@saveWorkflow');
@ -275,3 +278,19 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible'); WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
}); });
}); });
describe('Menu entry Push To Git', () => {
it('should not show up in the menu for members', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.visit(WorkflowPages.url);
WorkflowPage.actions.visit();
WorkflowPage.getters.workflowMenuItemGitPush().should('not.exist');
});
it('should show up for owners', () => {
cy.signin(INSTANCE_OWNER);
cy.visit(WorkflowPages.url);
WorkflowPage.actions.visit();
WorkflowPage.getters.workflowMenuItemGitPush().should('exist');
});
});

View file

@ -1,6 +1,8 @@
import { WorkflowPage, NDV } from '../pages'; import { WorkflowPage, NDV } from '../pages';
import { NodeCreator } from '../pages/features/node-creator';
const workflowPage = new WorkflowPage(); const workflowPage = new WorkflowPage();
const nodeCreatorFeature = new NodeCreator();
const ndv = new NDV(); const ndv = new NDV();
describe('HTTP Request node', () => { describe('HTTP Request node', () => {
@ -18,4 +20,40 @@ describe('HTTP Request node', () => {
ndv.getters.outputPanel().contains('fact'); ndv.getters.outputPanel().contains('fact');
}); });
describe('Credential-only HTTP Request Node variants', () => {
it('should render a modified HTTP Request Node', () => {
workflowPage.actions.addInitialNodeToCanvas('Manual');
workflowPage.getters.nodeCreatorPlusButton().click();
workflowPage.getters.nodeCreatorSearchBar().type('VirusTotal');
expect(nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'VirusTotal'));
expect(
nodeCreatorFeature.getters
.nodeItemDescription()
.first()
.should('have.text', 'HTTP request'),
);
nodeCreatorFeature.actions.selectNode('VirusTotal');
expect(ndv.getters.nodeNameContainer().should('contain.text', 'VirusTotal HTTP Request'));
expect(
ndv.getters
.parameterInput('url')
.find('input')
.should('contain.value', 'https://www.virustotal.com/api/v3/'),
);
// These parameters exist for normal HTTP Request Node, but are hidden for credential-only variants
expect(ndv.getters.parameterInput('authentication').should('not.exist'));
expect(ndv.getters.parameterInput('nodeCredentialType').should('not.exist'));
expect(
workflowPage.getters
.nodeCredentialsLabel()
.should('contain.text', 'Credential for VirusTotal'),
);
});
});
}); });

View file

@ -57,6 +57,6 @@ describe('Expression editor modal', () => {
it('should resolve $parameter[]', () => { it('should resolve $parameter[]', () => {
WorkflowPage.getters.expressionModalInput().clear(); WorkflowPage.getters.expressionModalInput().clear();
WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]'); WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]');
WorkflowPage.getters.expressionModalOutput().contains(/^get$/); WorkflowPage.getters.expressionModalOutput().should('have.text', 'getAll');
}); });
}); });

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,176 @@
{
"name": "Floating Nodes",
"nodes": [
{
"parameters": {},
"id": "d0eda550-2526-42a1-aa19-dee411c8acf9",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
700,
560
]
},
{
"parameters": {
"options": {}
},
"id": "30412165-1229-4b21-9890-05bfbd9952ab",
"name": "Node 1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
920,
560
]
},
{
"parameters": {
"options": {}
},
"id": "201cc8fc-3124-47a3-bc08-b3917c1ddcd9",
"name": "Node 2",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1100,
560
]
},
{
"parameters": {
"options": {}
},
"id": "a29802bb-a284-495d-9917-6c6e42fef01e",
"name": "Node 3",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1280,
560
]
},
{
"parameters": {
"options": {}
},
"id": "a95a72b3-8b39-44e2-a05b-d8d677741c80",
"name": "Node 4",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1440,
560
]
},
{
"parameters": {},
"id": "4674f10d-6144-4a17-bbbb-350c3974438e",
"name": "Chain",
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"typeVersion": 1,
"position": [
1580,
560
]
},
{
"parameters": {
"options": {}
},
"id": "58e12ea5-bd3e-4abf-abec-fcfb5c0a7955",
"name": "Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1,
"position": [
1600,
740
]
}
],
"pinData": {},
"connections": {
"When clicking \"Test Workflow\"": {
"main": [
[
{
"node": "Node 1",
"type": "main",
"index": 0
}
]
]
},
"Node 1": {
"main": [
[
{
"node": "Node 2",
"type": "main",
"index": 0
}
]
]
},
"Node 3": {
"main": [
[
{
"node": "Node 4",
"type": "main",
"index": 0
}
]
]
},
"Node 2": {
"main": [
[
{
"node": "Node 3",
"type": "main",
"index": 0
}
]
]
},
"Chain": {
"main": [
[]
]
},
"Model": {
"ai_languageModel": [
[
{
"node": "Chain",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Node 4": {
"main": [
[
{
"node": "Chain",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "2730d156-a98a-4ac8-b481-5c16361fdba2",
"id": "6bzXMGxHuxeEaqsA",
"meta": {
"instanceId": "1838be0fa0389fbaf5e2e4aaedab4ddc79abc4175b433401abb22a281001b853"
},
"tags": []
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,653 @@
{
"name": "Node IO filter",
"nodes": [
{
"parameters": {},
"id": "46770685-44d1-4aad-9107-1d790cf26b50",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
840,
180
]
},
{
"parameters": {
"options": {}
},
"id": "480e3832-2ce4-4118-9f7b-a8aed6017174",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1080,
180
]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{ $json.profile.name }}",
"operation": "contains",
"value2": "an"
}
]
}
},
"id": "4773d460-6ed9-49e1-a688-7e480f0fbacf",
"name": "IF",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
1300,
180
]
},
{
"parameters": {
"options": {}
},
"id": "d17dffe6-e29c-4c1a-8b4c-9e374dcd70ea",
"name": "True",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1560,
60
]
},
{
"parameters": {
"options": {}
},
"id": "893d6e79-feb4-4752-a6f8-e2e5f5163787",
"name": "False",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1560,
240
]
}
],
"pinData": {
"When clicking \"Test Workflow\"": [
{
"json": {
"id": "654cfa05fa51480dcb543b1a",
"email": "reese_hahn@kidgrease.coach",
"username": "reese94",
"profile": {
"name": "Reese Hahn",
"company": "Kidgrease",
"dob": "1994-06-18",
"address": "3 Richmond Street, Norfolk, Delaware",
"location": {
"lat": 22.507436,
"long": -50.812775
},
"about": "Cupidatat voluptate reprehenderit commodo mollit tempor sint id. Id exercitation id eiusmod dolore non non anim voluptate anim eu consectetur."
},
"apiKey": "a18592bf-1147-4b61-a70f-2ab90b60bb6e",
"roles": [
"guest"
],
"createdAt": "2010-10-04T09:57:59.240Z",
"updatedAt": "2010-10-05T09:57:59.240Z"
}
},
{
"json": {
"id": "654cfa055bea471bc4853158",
"email": "jeanne_boyd@hatology.gratis",
"username": "jeanne91",
"profile": {
"name": "Jeanne Boyd",
"company": "Hatology",
"dob": "1991-02-21",
"address": "81 Kingsway Place, Blairstown, Vermont",
"location": {
"lat": -57.665234,
"long": -41.301893
},
"about": "Proident pariatur non consequat cupidatat Lorem nisi est consequat dolor id eiusmod id. Amet culpa ex Lorem nostrud labore laboris culpa mollit dolor culpa ut."
},
"apiKey": "8a6056a6-0197-4920-858d-cb26f8c8a1e2",
"roles": [
"owner",
"admin"
],
"createdAt": "2011-11-06T09:05:41.945Z",
"updatedAt": "2011-11-07T09:05:41.945Z"
}
},
{
"json": {
"id": "654cfa05b012921c060dc5a5",
"email": "roslyn_underwood@portico.melbourne",
"username": "roslyn88",
"profile": {
"name": "Roslyn Underwood",
"company": "Portico",
"dob": "1988-04-30",
"address": "24 Schenck Street, Drytown, New Jersey",
"location": {
"lat": 11.797141,
"long": 10.751804
},
"about": "Duis excepteur minim consequat exercitation. Laboris occaecat cupidatat aliqua consequat occaecat."
},
"apiKey": "72d629f3-d613-4fd0-bbfe-3f67c8ad7af2",
"roles": [
"member",
"owner"
],
"createdAt": "2012-11-17T22:09:10.911Z",
"updatedAt": "2012-11-18T22:09:10.911Z"
}
},
{
"json": {
"id": "654cfa05df7b35968507efe6",
"email": "combs_hardy@acrodance.domains",
"username": "combs91",
"profile": {
"name": "Combs Hardy",
"company": "Acrodance",
"dob": "1991-04-30",
"address": "58 Pineapple Street, Falconaire, New Mexico",
"location": {
"lat": -62.922443,
"long": -159.493799
},
"about": "Magna qui minim velit magna est eiusmod aliquip elit aliquip excepteur. Laborum labore do ut et ut in incididunt do elit nostrud."
},
"apiKey": "d9807b9e-aee9-486d-9826-4e6c166bfbe4",
"roles": [
"owner",
"member"
],
"createdAt": "2014-04-13T13:02:09.319Z",
"updatedAt": "2014-04-14T13:02:09.319Z"
}
},
{
"json": {
"id": "654cfa05f2d4a0508a7c59c4",
"email": "terrell_peters@vantage.international",
"username": "terrell94",
"profile": {
"name": "Terrell Peters",
"company": "Vantage",
"dob": "1994-01-31",
"address": "10 Lafayette Walk, Vincent, Virginia",
"location": {
"lat": -62.267913,
"long": 29.682121
},
"about": "Eiusmod fugiat nulla ea tempor incididunt nulla nulla consectetur officia incididunt proident sint. Sunt duis non excepteur non."
},
"apiKey": "20b96df1-d882-4dea-a505-84d7ff296a6e",
"roles": [
"admin",
"guest"
],
"createdAt": "2010-12-09T08:24:56.517Z",
"updatedAt": "2010-12-10T08:24:56.517Z"
}
},
{
"json": {
"id": "654cfa0599fbabf3a05c7b14",
"email": "shari_winters@powernet.supply",
"username": "shari93",
"profile": {
"name": "Shari Winters",
"company": "Powernet",
"dob": "1993-03-10",
"address": "89 Aviation Road, Leyner, Indiana",
"location": {
"lat": 40.404704,
"long": -141.216235
},
"about": "Occaecat sit laboris elit laboris do anim culpa dolore exercitation enim. Non veniam sint exercitation irure."
},
"apiKey": "2b869ce9-3431-4edb-944d-9d9336b1eb4a",
"roles": [
"guest",
"admin"
],
"createdAt": "2014-10-15T15:56:55.873Z",
"updatedAt": "2014-10-16T15:56:55.873Z"
}
},
{
"json": {
"id": "654cfa050df18b4798ec95be",
"email": "rena_beasley@bitrex.ma",
"username": "rena90",
"profile": {
"name": "Rena Beasley",
"company": "Bitrex",
"dob": "1990-01-09",
"address": "78 Forbell Street, Homeland, Maine",
"location": {
"lat": 46.047548,
"long": 4.128049
},
"about": "Lorem aliqua veniam duis ut cillum ad sunt mollit incididunt elit. Ipsum incididunt et magna incididunt quis duis amet duis occaecat laborum nulla et commodo nisi."
},
"apiKey": "17e350f8-1020-4344-bbd7-ceb62cd44edb",
"roles": [
"member",
"owner"
],
"createdAt": "2010-04-22T13:35:24.838Z",
"updatedAt": "2010-04-23T13:35:24.838Z"
}
},
{
"json": {
"id": "654cfa0595243d2b7b1ea22a",
"email": "sally_gentry@eventex.maif",
"username": "sally93",
"profile": {
"name": "Sally Gentry",
"company": "Eventex",
"dob": "1993-04-03",
"address": "54 Plaza Street, Greenbackville, North Carolina",
"location": {
"lat": -20.529121,
"long": 73.533118
},
"about": "Laborum sit exercitation sint laborum. Fugiat sit ipsum ullamco sint do dolore in sunt incididunt adipisicing magna ullamco aute."
},
"apiKey": "746b6ab3-c63f-44df-bb99-9de48f8e43c4",
"roles": [
"owner",
"guest"
],
"createdAt": "2011-09-18T13:18:49.655Z",
"updatedAt": "2011-09-19T13:18:49.655Z"
}
},
{
"json": {
"id": "654cfa05cdea66c87bb01439",
"email": "battle_duran@jasper.property",
"username": "battle88",
"profile": {
"name": "Battle Duran",
"company": "Jasper",
"dob": "1988-11-04",
"address": "34 Amherst Street, Corriganville, Nevada",
"location": {
"lat": 74.391489,
"long": -98.421464
},
"about": "Nostrud occaecat laborum aliquip sint est minim id aliquip adipisicing dolor. Aute velit amet officia anim sint anim aliquip."
},
"apiKey": "b22a3ddd-d540-4df0-9ce5-e837bc6a6a10",
"roles": [
"member"
],
"createdAt": "2012-08-31T19:14:37.463Z",
"updatedAt": "2012-09-01T19:14:37.463Z"
}
},
{
"json": {
"id": "654cfa05e9c13e25d41d4135",
"email": "petty_moore@neurocell.shriram",
"username": "petty91",
"profile": {
"name": "Petty Moore",
"company": "Neurocell",
"dob": "1991-03-10",
"address": "78 Interborough Parkway, Grill, Texas",
"location": {
"lat": -79.817761,
"long": -36.728201
},
"about": "Dolor occaecat anim est Lorem culpa fugiat id aliqua sint. Sit nisi do exercitation do voluptate exercitation in."
},
"apiKey": "4b341cfb-a83c-4f2a-9f4d-11cd747b8783",
"roles": [
"admin"
],
"createdAt": "2012-01-02T21:28:22.431Z",
"updatedAt": "2012-01-03T21:28:22.431Z"
}
},
{
"json": {
"id": "654cfa052890c7b4d510d3d4",
"email": "matilda_kelley@senmei.in",
"username": "matilda93",
"profile": {
"name": "Matilda Kelley",
"company": "Senmei",
"dob": "1993-02-04",
"address": "29 Stuart Street, Henrietta, New York",
"location": {
"lat": 40.788206,
"long": -135.821558
},
"about": "Dolor veniam ex ullamco deserunt reprehenderit nostrud sunt culpa cupidatat qui labore deserunt. In ad anim laboris amet labore duis consequat nostrud eiusmod."
},
"apiKey": "dcf40383-a00a-43ef-8bd0-4af7e70413bd",
"roles": [
"owner",
"guest"
],
"createdAt": "2014-03-28T22:07:39.636Z",
"updatedAt": "2014-03-29T22:07:39.636Z"
}
},
{
"json": {
"id": "654cfa05af129db469473bf1",
"email": "savannah_hardin@exoblue.kn",
"username": "savannah89",
"profile": {
"name": "Savannah Hardin",
"company": "Exoblue",
"dob": "1989-07-01",
"address": "44 Navy Walk, Fresno, Kentucky",
"location": {
"lat": 75.679679,
"long": -58.534947
},
"about": "Id eiusmod eu elit consequat quis anim veniam officia anim ipsum. Sunt ex sit ipsum id est eu."
},
"apiKey": "98d6abb7-e4aa-4b3b-8958-ff3c4d672f1d",
"roles": [
"guest",
"member"
],
"createdAt": "2011-04-15T00:55:02.325Z",
"updatedAt": "2011-04-16T00:55:02.325Z"
}
},
{
"json": {
"id": "654cfa055dfa731b01573a67",
"email": "abbott_gallegos@katakana.dad",
"username": "abbott91",
"profile": {
"name": "Abbott Gallegos",
"company": "Katakana",
"dob": "1991-03-04",
"address": "85 Indiana Place, Forestburg, Michigan",
"location": {
"lat": -5.417414,
"long": -4.557904
},
"about": "Adipisicing amet ullamco aliquip velit nostrud qui non pariatur Lorem. Culpa ut deserunt esse quis magna."
},
"apiKey": "3cf92c24-6193-4cc9-85fc-78e4ad9d6e13",
"roles": [
"guest",
"owner"
],
"createdAt": "2011-06-01T16:38:39.316Z",
"updatedAt": "2011-06-02T16:38:39.316Z"
}
},
{
"json": {
"id": "654cfa05386de2e6d75c1694",
"email": "short_brennan@hyplex.tc",
"username": "short92",
"profile": {
"name": "Short Brennan",
"company": "Hyplex",
"dob": "1992-04-19",
"address": "21 Irving Place, Hinsdale, Northern Mariana Islands",
"location": {
"lat": 57.340225,
"long": -7.021582
},
"about": "Mollit dolor dolore deserunt anim minim adipisicing eiusmod velit tempor id veniam cupidatat. Magna veniam consequat incididunt ut quis culpa excepteur tempor eiusmod consectetur excepteur."
},
"apiKey": "07bf533d-4a31-4e78-9d6e-d46160479069",
"roles": [
"admin",
"member"
],
"createdAt": "2014-03-10T19:25:02.217Z",
"updatedAt": "2014-03-11T19:25:02.217Z"
}
},
{
"json": {
"id": "654cfa05fd2a878d43bb45cd",
"email": "bowers_cooke@iplax.ci",
"username": "bowers92",
"profile": {
"name": "Bowers Cooke",
"company": "Iplax",
"dob": "1992-07-05",
"address": "83 Greenpoint Avenue, Marion, Georgia",
"location": {
"lat": 64.261022,
"long": -58.493714
},
"about": "Deserunt ipsum fugiat tempor sunt eu ea laboris ad magna ex laborum laboris. Ullamco nostrud qui exercitation aute consectetur irure."
},
"apiKey": "a3ecc58b-f292-4de1-b6e5-014345a76a7a",
"roles": [
"member",
"owner"
],
"createdAt": "2010-06-20T16:34:56.467Z",
"updatedAt": "2010-06-21T16:34:56.467Z"
}
},
{
"json": {
"id": "654cfa05a6de547367990f9c",
"email": "tara_rutledge@escenta.lc",
"username": "tara90",
"profile": {
"name": "Tara Rutledge",
"company": "Escenta",
"dob": "1990-08-11",
"address": "25 Butler Place, Frierson, Missouri",
"location": {
"lat": -32.176783,
"long": 67.345415
},
"about": "Aute sunt laborum anim ex non pariatur nisi minim tempor adipisicing. Excepteur irure non amet eiusmod et excepteur."
},
"apiKey": "22da9647-a7b7-4815-91bb-d5101fc90e55",
"roles": [
"member"
],
"createdAt": "2013-09-06T21:41:53.287Z",
"updatedAt": "2013-09-07T21:41:53.287Z"
}
},
{
"json": {
"id": "654cfa053778601ad57f22cd",
"email": "elva_chapman@bytrex.gg",
"username": "elva90",
"profile": {
"name": "Elva Chapman",
"company": "Bytrex",
"dob": "1990-05-31",
"address": "4 Royce Place, Advance, New Hampshire",
"location": {
"lat": -28.393464,
"long": -28.622091
},
"about": "Est sit deserunt Lorem amet voluptate elit reprehenderit occaecat est eiusmod eu reprehenderit laborum. Pariatur magna occaecat et excepteur est excepteur consectetur ad nulla."
},
"apiKey": "4d242fa4-ac69-42f1-8f12-ec19d9c6d632",
"roles": [
"owner",
"admin"
],
"createdAt": "2011-04-05T04:04:31.524Z",
"updatedAt": "2011-04-06T04:04:31.524Z"
}
},
{
"json": {
"id": "654cfa054c6abbc57efcb100",
"email": "pitts_meyer@unisure.tui",
"username": "pitts93",
"profile": {
"name": "Pitts Meyer",
"company": "Unisure",
"dob": "1993-06-12",
"address": "47 Columbus Place, Cade, Alaska",
"location": {
"lat": 56.723675,
"long": 158.093389
},
"about": "Non ea pariatur excepteur nostrud elit quis qui. Dolore aute velit ipsum officia ea pariatur incididunt non elit tempor duis consequat."
},
"apiKey": "82a88344-d289-447c-81b5-1ae10cd1994b",
"roles": [
"guest",
"admin"
],
"createdAt": "2014-05-15T06:38:59.269Z",
"updatedAt": "2014-05-16T06:38:59.269Z"
}
},
{
"json": {
"id": "654cfa0527e7ce14e421d9cd",
"email": "delia_figueroa@overplex.um",
"username": "delia89",
"profile": {
"name": "Delia Figueroa",
"company": "Overplex",
"dob": "1989-04-22",
"address": "12 Nova Court, Taft, Ohio",
"location": {
"lat": -32.990583,
"long": -4.598863
},
"about": "Cupidatat fugiat veniam eu proident excepteur deserunt ad esse fugiat deserunt. Non velit cillum velit veniam ex minim eiusmod tempor excepteur voluptate adipisicing nostrud."
},
"apiKey": "b3a7747b-24a0-4039-8a21-56e83441a660",
"roles": [
"admin",
"guest"
],
"createdAt": "2014-09-20T03:40:10.190Z",
"updatedAt": "2014-09-21T03:40:10.190Z"
}
},
{
"json": {
"id": "654cfa05cf60000cbca6dca4",
"email": "kristina_fulton@portaline.engineer",
"username": "kristina88",
"profile": {
"name": "Kristina Fulton",
"company": "Portaline",
"dob": "1988-07-25",
"address": "50 Laurel Avenue, Greenwich, Palau",
"location": {
"lat": 44.118984,
"long": 41.518949
},
"about": "Id incididunt officia exercitation ipsum id cillum consectetur. Veniam enim voluptate ut proident ex."
},
"apiKey": "c106dbf0-bfc0-461d-b1d7-1840fe8e1cbc",
"roles": [
"admin",
"member"
],
"createdAt": "2010-04-10T08:06:27.028Z",
"updatedAt": "2010-04-11T08:06:27.028Z"
}
},
{
"json": {
"id": "654cfa0501fe5691d620f570",
"email": "gould_noel@gonkle.gmx",
"username": "gould91",
"profile": {
"name": "Gould Noel",
"company": "Gonkle",
"dob": "1991-10-08",
"address": "33 Crooke Avenue, Idamay, Oklahoma",
"location": {
"lat": -11.398731,
"long": 34.706948
},
"about": "Veniam esse tempor aute quis mollit consequat Lorem. Nostrud ea dolore laboris Lorem elit est do nisi Lorem minim reprehenderit culpa."
},
"apiKey": "1089783d-32ae-4102-8ac5-1e7f6cebe3c1",
"roles": [
"guest",
"admin"
],
"createdAt": "2011-12-30T20:24:19.620Z",
"updatedAt": "2011-12-31T20:24:19.620Z"
}
}
]
},
"connections": {
"When clicking \"Test Workflow\"": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "IF",
"type": "main",
"index": 0
}
]
]
},
"IF": {
"main": [
[
{
"node": "True",
"type": "main",
"index": 0
}
],
[
{
"node": "False",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "9812dda2-cc1b-4458-97d8-21ccb18c90d1",
"id": "WNq486x7DpV1MPRH",
"meta": {
"instanceId": "8a47b83b4479b11330fdf21ccc96d4a8117035a968612e452b4c87bfd09c16c7"
},
"tags": []
}

View file

@ -13,8 +13,7 @@
"feat:advancedExecutionFilters": true, "feat:advancedExecutionFilters": true,
"quota:users": -1, "quota:users": -1,
"quota:maxVariables": -1, "quota:maxVariables": -1,
"feat:variables": true, "feat:variables": true
"feat:apiDisabled": true
}, },
"metadata": { "metadata": {
"version": "v1", "version": "v1",

View file

@ -0,0 +1,313 @@
{
"name": "Schedule + pinned",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"id": "66358c29-b263-43dd-be25-3b068b0a88eb",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.1,
"position": [
660,
340
]
},
{
"parameters": {
"options": {}
},
"id": "6d903354-4e59-4032-81fe-426a5d6ec33c",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
860,
240
]
},
{
"parameters": {
"options": {}
},
"id": "d8a1e9cf-81d3-400f-97d4-ad6167e7b236",
"name": "Edit Fields1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
860,
440
]
},
{
"parameters": {
"options": {}
},
"id": "bdc41148-067e-4649-8f21-5707b128d877",
"name": "Edit Fields2",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1080,
440
]
},
{
"parameters": {
"options": {}
},
"id": "d5a4337f-a6b3-4b51-9b02-e668593d9ae8",
"name": "Edit Fields3",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1300,
440
]
},
{
"parameters": {
"options": {}
},
"id": "fbc23f60-e7f6-4423-9329-33b0e4809a9a",
"name": "Edit Fields4",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1500,
440
]
},
{
"parameters": {
"options": {}
},
"id": "eaee47b0-94ec-4137-bfeb-a6c1a2c63f81",
"name": "Edit Fields5",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1080,
240
]
},
{
"parameters": {
"options": {}
},
"id": "eabb6308-21e9-4e59-8f74-9220a03c3186",
"name": "Edit Fields6",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1300,
240
]
},
{
"parameters": {
"options": {}
},
"id": "8812a45b-5545-4080-aad8-8e9f7b17ecd7",
"name": "Edit Fields7",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1500,
240
]
},
{
"parameters": {
"options": {}
},
"id": "d5ea3c5b-0b3e-4514-93e1-9c88563bab5c",
"name": "Edit Fields9",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1700,
240
]
},
{
"parameters": {
"options": {}
},
"id": "7af34474-5cd0-40b1-abea-850858e3b495",
"name": "Edit Fields10",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1700,
440
]
}
],
"pinData": {
"Schedule Trigger": [
{
"json": {
"name": "First item",
"code": 1
}
},
{
"json": {
"name": "Second item",
"code": 2
}
}
],
"Edit Fields7": [
{
"json": {
"name": "First item",
"code": 1
}
},
{
"json": {
"name": "Second item",
"code": 2
}
}
],
"Edit Fields2": [
{
"json": {
"name": "First item",
"code": 1
}
},
{
"json": {
"name": "Second item",
"code": 2
}
}
]
},
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
},
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields1": {
"main": [
[
{
"node": "Edit Fields2",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields2": {
"main": [
[
{
"node": "Edit Fields3",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields3": {
"main": [
[
{
"node": "Edit Fields4",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields5": {
"main": [
[
{
"node": "Edit Fields6",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields6": {
"main": [
[
{
"node": "Edit Fields7",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "Edit Fields5",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields7": {
"main": [
[
{
"node": "Edit Fields9",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields4": {
"main": [
[
{
"node": "Edit Fields10",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "9b6c68c0-f94f-45bc-a604-bf97d17a47ac",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "8a47b83b4479b11330fdf21ccc96d4a8117035a968612e452b4c87bfd09c16c7"
},
"id": "nWzcnYUb3AVaZpHG",
"tags": []
}

View file

@ -0,0 +1,655 @@
{
"sections": [
{
"name": "Lead enrichment",
"description": "Explore curated lead enrichment workflows or start fresh with a blank canvas",
"workflows": [
{
"title": "Score new leads with AI from Facebook Lead Ads with AI and get notifications for high scores on Slack",
"description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.",
"preview": {
"nodes": [
{
"parameters": {
"operation": "create",
"base": {
"__rl": true,
"mode": "list",
"value": ""
},
"table": {
"__rl": true,
"mode": "list",
"value": ""
},
"columns": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"id": "b09d4f4d-19fa-43de-8148-2d430a04956f",
"name": "Airtable",
"type": "n8n-nodes-base.airtable",
"typeVersion": 2,
"position": [
1800,
740
]
},
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
920,
740
]
},
{
"parameters": {
"options": {}
},
"id": "b4c089ee-2adb-435e-8d48-47012c981a11",
"name": "Get image",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
1140,
740
]
},
{
"parameters": {
"operation": "extractHtmlContent",
"options": {}
},
"id": "04ca2f61-b930-4fbc-b467-3470c0d93d64",
"name": "Extract Information",
"type": "n8n-nodes-base.html",
"typeVersion": 1,
"position": [
1360,
740
]
},
{
"parameters": {
"options": {}
},
"id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce",
"name": "Set Information",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1580,
740
]
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"main": [
[
{
"node": "Get image",
"type": "main",
"index": 0
}
]
]
},
"Get image": {
"main": [
[
{
"node": "Extract Information",
"type": "main",
"index": 0
}
]
]
},
"Extract Information": {
"main": [
[
{
"node": "Set Information",
"type": "main",
"index": 0
}
]
]
},
"Set Information": {
"main": [
[
{
"node": "Airtable",
"type": "main",
"index": 0
}
]
]
}
}
},
"nodes": [
{
"id": 24,
"icon": "fa:code-branch",
"defaults": {
"color": "#00bbcc"
},
"iconData": {
"icon": "code-branch",
"type": "icon"
},
"displayName": "Merge"
}
]
},
{
"title": "Verify the email address every time a contact is created in HubSpot",
"description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.",
"preview": {
"nodes": [
{
"parameters": {
"operation": "create",
"base": {
"__rl": true,
"mode": "list",
"value": ""
},
"table": {
"__rl": true,
"mode": "list",
"value": ""
},
"columns": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"id": "b09d4f4d-19fa-43de-8148-2d430a04956f",
"name": "Airtable",
"type": "n8n-nodes-base.airtable",
"typeVersion": 2,
"position": [
1800,
740
]
},
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
920,
740
]
},
{
"parameters": {
"options": {}
},
"id": "b4c089ee-2adb-435e-8d48-47012c981a11",
"name": "Get image",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
1140,
740
]
},
{
"parameters": {
"operation": "extractHtmlContent",
"options": {}
},
"id": "04ca2f61-b930-4fbc-b467-3470c0d93d64",
"name": "Extract Information",
"type": "n8n-nodes-base.html",
"typeVersion": 1,
"position": [
1360,
740
]
},
{
"parameters": {
"options": {}
},
"id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce",
"name": "Set Information",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1580,
740
]
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"main": [
[
{
"node": "Get image",
"type": "main",
"index": 0
}
]
]
},
"Get image": {
"main": [
[
{
"node": "Extract Information",
"type": "main",
"index": 0
}
]
]
},
"Extract Information": {
"main": [
[
{
"node": "Set Information",
"type": "main",
"index": 0
}
]
]
},
"Set Information": {
"main": [
[
{
"node": "Airtable",
"type": "main",
"index": 0
}
]
]
}
}
},
"nodes": [
{
"id": 14,
"icon": "fa:code",
"name": "n8n-nodes-base.function",
"defaults": {
"name": "Function",
"color": "#FF9922"
},
"iconData": {
"icon": "code",
"type": "icon"
},
"categories": [
{
"id": 5,
"name": "Development"
},
{
"id": 9,
"name": "Core Nodes"
}
],
"displayName": "Function",
"typeVersion": 1
},
{
"id": 24,
"icon": "fa:code-branch",
"name": "n8n-nodes-base.merge",
"defaults": {
"name": "Merge",
"color": "#00bbcc"
},
"iconData": {
"icon": "code-branch",
"type": "icon"
},
"categories": [
{
"id": 9,
"name": "Core Nodes"
}
],
"displayName": "Merge",
"typeVersion": 2
}
]
},
{
"title": "Enrich leads from HubSpot with company information via OpenAi",
"description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.",
"preview": {
"nodes": [
{
"parameters": {
"operation": "create",
"base": {
"__rl": true,
"mode": "list",
"value": ""
},
"table": {
"__rl": true,
"mode": "list",
"value": ""
},
"columns": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"id": "b09d4f4d-19fa-43de-8148-2d430a04956f",
"name": "Airtable",
"type": "n8n-nodes-base.airtable",
"typeVersion": 2,
"position": [
1800,
740
]
},
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
920,
740
]
},
{
"parameters": {
"options": {}
},
"id": "b4c089ee-2adb-435e-8d48-47012c981a11",
"name": "Get image",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
1140,
740
]
},
{
"parameters": {
"operation": "extractHtmlContent",
"options": {}
},
"id": "04ca2f61-b930-4fbc-b467-3470c0d93d64",
"name": "Extract Information",
"type": "n8n-nodes-base.html",
"typeVersion": 1,
"position": [
1360,
740
]
},
{
"parameters": {
"options": {}
},
"id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce",
"name": "Set Information",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1580,
740
]
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"main": [
[
{
"node": "Get image",
"type": "main",
"index": 0
}
]
]
},
"Get image": {
"main": [
[
{
"node": "Extract Information",
"type": "main",
"index": 0
}
]
]
},
"Extract Information": {
"main": [
[
{
"node": "Set Information",
"type": "main",
"index": 0
}
]
]
},
"Set Information": {
"main": [
[
{
"node": "Airtable",
"type": "main",
"index": 0
}
]
]
}
}
},
"nodes": [
{
"id": 14,
"icon": "fa:code",
"defaults": {
"name": "Function",
"color": "#FF9922"
},
"iconData": {
"icon": "code",
"type": "icon"
},
"displayName": "Function"
}
]
},
{
"title": "Score new lead submissions from Facebook Lead Ads with AI and notify me on Slack when it is a high score lead",
"description": "This workflow will help you save tons of time and will notify you fully automatically about the most important incoming leads from Facebook Lead Ads. The workflow will automatically fire for every submission. It will then take the name, company, and email information, enrich the submitter via AI, and score it based on metrics that you can easily set.",
"preview": {
"nodes": [
{
"parameters": {
"operation": "create",
"base": {
"__rl": true,
"mode": "list",
"value": ""
},
"table": {
"__rl": true,
"mode": "list",
"value": ""
},
"columns": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": []
},
"options": {}
},
"id": "b09d4f4d-19fa-43de-8148-2d430a04956f",
"name": "Airtable",
"type": "n8n-nodes-base.airtable",
"typeVersion": 2,
"position": [
1800,
740
]
},
{
"parameters": {},
"id": "551313bb-1e01-4133-9956-e6f09968f2ce",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
920,
740
]
},
{
"parameters": {
"options": {}
},
"id": "b4c089ee-2adb-435e-8d48-47012c981a11",
"name": "Get image",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
1140,
740
]
},
{
"parameters": {
"operation": "extractHtmlContent",
"options": {}
},
"id": "04ca2f61-b930-4fbc-b467-3470c0d93d64",
"name": "Extract Information",
"type": "n8n-nodes-base.html",
"typeVersion": 1,
"position": [
1360,
740
]
},
{
"parameters": {
"options": {}
},
"id": "d1a77493-c579-4ac4-b6a7-708eea2bf8ce",
"name": "Set Information",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1580,
740
]
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"main": [
[
{
"node": "Get image",
"type": "main",
"index": 0
}
]
]
},
"Get image": {
"main": [
[
{
"node": "Extract Information",
"type": "main",
"index": 0
}
]
]
},
"Extract Information": {
"main": [
[
{
"node": "Set Information",
"type": "main",
"index": 0
}
]
]
},
"Set Information": {
"main": [
[
{
"node": "Airtable",
"type": "main",
"index": 0
}
]
]
}
}
},
"nodes": [
{
"id": 14,
"icon": "fa:code",
"defaults": {
"name": "Function",
"color": "#FF9922"
},
"iconData": {
"icon": "code",
"type": "icon"
},
"displayName": "Function"
},
{
"id": 24,
"icon": "fa:code-branch",
"defaults": {
"name": "Merge",
"color": "#00bbcc"
},
"iconData": {
"icon": "code-branch",
"type": "icon"
},
"displayName": "Merge"
}
]
}
]
}
]
}

View file

@ -0,0 +1,177 @@
{
"workflow": {
"id": 1205,
"name": "Promote new Shopify products on Twitter and Telegram",
"views": 478,
"recentViews": 9880,
"totalViews": 478,
"createdAt": "2021-08-24T10:40:50.007Z",
"description": "This workflow automatically promotes your new Shopify products on Twitter and Telegram. This workflow is also featured in the blog post [*6 e-commerce workflows to power up your Shopify store*](https://n8n.io/blog/no-code-ecommerce-workflow-automations/#promote-your-new-products-on-social-media).\n\n## Prerequisites\n\n- A Shopify account and [credentials](https://docs.n8n.io/integrations/credentials/shopify/)\n- A Twitter account and [credentials](https://docs.n8n.io/integrations/credentials/twitter/)\n- A Telegram account and [credentials](https://docs.n8n.io/integrations/credentials/telegram/) for the channel you want to send messages to.\n\n## Nodes\n\n- [Shopify Trigger node](https://docs.n8n.io/integrations/trigger-nodes/n8n-nodes-base.shopifytrigger/) triggers the workflow when you create a new product in Shopify.\n- [Twitter node](https://docs.n8n.io/integrations/nodes/n8n-nodes-base.twitter/) posts a tweet with the text \"Hey there, my design is now on a new product! Visit my {shop name} to get this cool {product title} (and check out more {product type})\".\n- [Telegram node](https://docs.n8n.io/integrations/nodes/n8n-nodes-base.telegram/) posts a message with the same text as above in a Telegram channel.",
"workflow": {
"nodes": [
{
"name": "Twitter",
"type": "n8n-nodes-base.twitter",
"position": [
720,
-220
],
"parameters": {
"text": "=Hey there, my design is now on a new product ✨\nVisit my {{$json[\"vendor\"]}} shop to get this cool{{$json[\"title\"]}} (and check out more {{$json[\"product_type\"]}}) 🛍️",
"additionalFields": {}
},
"credentials": {
"twitterOAuth1Api": "twitter"
},
"typeVersion": 1
},
{
"name": "Telegram",
"type": "n8n-nodes-base.telegram",
"position": [
720,
-20
],
"parameters": {
"text": "=Hey there, my design is now on a new product!\nVisit my {{$json[\"vendor\"]}} shop to get this cool{{$json[\"title\"]}} (and check out more {{$json[\"product_type\"]}})",
"chatId": "123456",
"additionalFields": {}
},
"credentials": {
"telegramApi": "telegram_habot"
},
"typeVersion": 1
},
{
"name": "product created",
"type": "n8n-nodes-base.shopifyTrigger",
"position": [
540,
-110
],
"webhookId": "2a7e0e50-8f09-4a2b-bf54-a849a6ac4fe0",
"parameters": {
"topic": "products/create"
},
"credentials": {
"shopifyApi": "shopify_nodeqa"
},
"typeVersion": 1
}
],
"connections": {
"product created": {
"main": [
[
{
"node": "Twitter",
"type": "main",
"index": 0
},
{
"node": "Telegram",
"type": "main",
"index": 0
}
]
]
}
}
},
"workflowInfo": {
"nodeCount": 3,
"nodeTypes": {
"n8n-nodes-base.twitter": {
"count": 1
},
"n8n-nodes-base.telegram": {
"count": 1
},
"n8n-nodes-base.shopifyTrigger": {
"count": 1
}
}
},
"user": {
"username": "lorenanda"
},
"nodes": [
{
"id": 49,
"icon": "file:telegram.svg",
"name": "n8n-nodes-base.telegram",
"defaults": {
"name": "Telegram"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNjYgNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTAgMzJjMCAxNy42NzMgMTQuMzI3IDMyIDMyIDMyczMyLTE0LjMyNyAzMi0zMlM0OS42NzMgMCAzMiAwIDAgMTQuMzI3IDAgMzIiIGZpbGw9IiMzN2FlZTIiLz48cGF0aCBkPSJNMjEuNjYxIDM0LjMzOGwzLjc5NyAxMC41MDhzLjQ3NS45ODMuOTgzLjk4MyA4LjA2OC03Ljg2NCA4LjA2OC03Ljg2NGw4LjQwNy0xNi4yMzctMjEuMTE5IDkuODk4eiIgZmlsbD0iI2M4ZGFlYSIvPjxwYXRoIGQ9Ik0yNi42OTUgMzcuMDM0bC0uNzI5IDcuNzQ2cy0uMzA1IDIuMzczIDIuMDY4IDBsNC42NDQtNC4yMDMiIGZpbGw9IiNhOWM2ZDgiLz48cGF0aCBkPSJNMjEuNzMgMzQuNzEybC03LjgwOS0yLjU0NXMtLjkzMi0uMzc4LS42MzMtMS4yMzdjLjA2Mi0uMTc3LjE4Ni0uMzI4LjU1OS0uNTg4IDEuNzMxLTEuMjA2IDMyLjAyOC0xMi4wOTYgMzIuMDI4LTEyLjA5NnMuODU2LS4yODggMS4zNjEtLjA5N2MuMjMxLjA4OC4zNzguMTg3LjUwMy41NDguMDQ1LjEzMi4wNzEuNDExLjA2OC42ODktLjAwMy4yMDEtLjAyNy4zODYtLjA0NS42NzgtLjE4NCAyLjk3OC01LjcwNiAyNS4xOTgtNS43MDYgMjUuMTk4cy0uMzMgMS4zLTEuNTE0IDEuMzQ1Yy0uNDMyLjAxNi0uOTU2LS4wNzEtMS41ODItLjYxLTIuMzIzLTEuOTk4LTEwLjM1Mi03LjM5NC0xMi4xMjYtOC41OGEuMzQuMzQgMCAwMS0uMTQ2LS4yMzljLS4wMjUtLjEyNS4xMDgtLjI4LjEwOC0uMjhzMTMuOTgtMTIuNDI3IDE0LjM1Mi0xMy43MzFjLjAyOS0uMTAxLS4wNzktLjE1MS0uMjI2LS4xMDctLjkyOS4zNDItMTcuMDI1IDEwLjUwNi0xOC44MDEgMTEuNjI5LS4xMDQuMDY2LS4zOTUuMDIzLS4zOTUuMDIzIi8+PC9nPjwvc3ltYm9sPjwvc3ZnPg=="
},
"categories": [
{
"id": 6,
"name": "Communication"
}
],
"displayName": "Telegram",
"typeVersion": 1
},
{
"id": 107,
"icon": "file:shopify.svg",
"name": "n8n-nodes-base.shopifyTrigger",
"defaults": {
"name": "Shopify Trigger"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgNTggNjYiIGZpbGw9IiNmZmYiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBzdHJva2U9Im5vbmUiIGZpbGwtcnVsZT0ibm9uemVybyI+PHBhdGggZD0iTTQ5LjI1NSAxMi40ODRhLjYzMy42MzMgMCAwMC0uNTY0LS41MjdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdjLS4zNC0uMzc2LTEuMDkyLS4yNjYtMS4zNzYtLjE4OC0uMDM3IDAtLjc1Mi4yMjUtMS45MjIuNjA1LTEuMTM3LTMuMy0zLjE1LTYuMzA2LTYuNjk2LTYuMzA2aC0uMzAzQzI4LjQzOC42MDUgMjcuMTk0IDAgMjYuMTQ0IDBjLTguMjU2LjAzNy0xMi4yIDEwLjMzMy0xMy40MzQgMTUuNTk0bC01Ljc3IDEuNzdjLTEuNzcuNTY0LTEuODM1LjYwNS0yLjA3MyAyLjI5M0wwIDU3LjE3NSAzNi40NjggNjRsMTkuNzYzLTQuMjZjMC0uMDM3LTYuOTQtNDYuODk3LTYuOTc2LTQ3LjI1NXpNMzQuNDMxIDguODZjLS45MTcuMzAzLTEuOTYzLjYwNS0zLjEuOTQ1di0uNjhhMTUuMDMgMTUuMDMgMCAwMC0uNzUyLTQuOTk5YzEuODQ4LjI4NCAzLjEgMi4zNTcgMy44NDMgNC43MzN6bS02LjA2OC00LjI5OGMuNjAzIDEuNzc4Ljg4MyAzLjY1LjgyNiA1LjUyN3YuMzRsLTYuMzc1IDEuOTYzYzEuMjQ4LTQuNjYgMy41NS02Ljk2MiA1LjU1LTcuODN6bS0yLjQ1LTIuMjkzYTEuOTQgMS45NCAwIDAxMS4wNTUuMzM5Yy0yLjY2IDEuMjM4LTUuNDcyIDQuMzY2LTYuNjc4IDEwLjYyN2wtNS4wNDUgMS41NDZDMTYuNjY4IDEwLjAzIDE5Ljk4OCAyLjI2IDI1LjkxIDIuMjZ6IiBmaWxsPSIjOTViZjQ3Ii8+PHBhdGggZD0iTTQ4LjY5MSAxMS45NTdjLS4yMjUtLjAzNy01LjE3LS4zNzYtNS4xNy0uMzc2bC0zLjc3LTMuNzdhLjc1My43NTMgMCAwMC0uNTI3LS4yMjVMMzYuNDcyIDY0bDE5Ljc2My00LjI2LTYuOTgtNDcuMjE4YS42OC42OCAwIDAwLS41NjQtLjU2NHoiIGZpbGw9IiM1ZThlM2UiLz48cGF0aCBkPSJNMjkuNzU4IDIyLjlsLTIuNDU0IDcuMjQyYTExLjM2IDExLjM2IDAgMDAtNC43NTItMS4xMzNjLTMuODQ4IDAtNC4wMzYgMi40MTItNC4wMzYgMy4wMTggMCAzLjI5OCA4LjYzNiA0LjU2NCA4LjYzNiAxMi4zMzMgMCA2LjEtMy44ODUgMTAuMDMtOS4xIDEwLjAzLTYuMjYgMC05LjQ2Ny0zLjg4NS05LjQ2Ny0zLjg4NWwxLjY2NS01LjU1czMuMjggMi44MyA2LjA3MyAyLjgzYTIuNDcgMi40NyAwIDAwMi41NjQtMi40OWMwLTQuMzQtNy4xLTQuNTI3LTcuMS0xMS42MTggMC01Ljk2MiA0LjI5OC0xMS43NyAxMi45MzQtMTEuNzcgMy4zOTQuMDUgNS4wMTggMSA1LjAxOCAxeiIvPjwvZz48L3N5bWJvbD48L3N2Zz4="
},
"categories": [
{
"id": 2,
"name": "Sales"
}
],
"displayName": "Shopify Trigger",
"typeVersion": 1
},
{
"id": 325,
"icon": "file:x.svg",
"name": "n8n-nodes-base.twitter",
"defaults": {
"name": "X"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE4LjI0NCAyLjI1aDMuMzA4bC03LjIyNyA4LjI2IDguNTAyIDExLjI0SDE2LjE3bC01LjIxNC02LjgxN0w0Ljk5IDIxLjc1SDEuNjhsNy43My04LjgzNUwxLjI1NCAyLjI1SDguMDhsNC43MTMgNi4yMzF6bS0xLjE2MSAxNy41MmgxLjgzM0w3LjA4NCA0LjEyNkg1LjExN3oiPjwvcGF0aD48L3N2Zz4K"
},
"categories": [
{
"id": 1,
"name": "Marketing & Content"
}
],
"displayName": "X (Formerly Twitter)",
"typeVersion": 2
}
],
"categories": [
{
"id": 2,
"name": "Sales"
},
{
"id": 19,
"name": "Marketing & Growth"
}
],
"image": [
{
"id": 527,
"url": "https://n8niostorageaccount.blob.core.windows.net/n8nio-strapi-blobs-prod/assets/89a078b208fe4c6181902608b1cd1332.png"
}
]
}
}

File diff suppressed because one or more lines are too long

View file

@ -40,7 +40,7 @@
{ {
"parameters": {}, "parameters": {},
"id": "ef63cdc5-50bc-4525-9873-7e7f7589a60e", "id": "ef63cdc5-50bc-4525-9873-7e7f7589a60e",
"name": "When clicking \"Execute Workflow\"", "name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger", "type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
@ -50,7 +50,6 @@
}, },
{ {
"parameters": { "parameters": {
"operation": "sort",
"sortFieldsUi": { "sortFieldsUi": {
"sortField": [ "sortField": [
{ {
@ -61,9 +60,9 @@
"options": {} "options": {}
}, },
"id": "555a150c-d735-4331-b628-c1f1cfed2da1", "id": "555a150c-d735-4331-b628-c1f1cfed2da1",
"name": "Item Lists", "name": "Sort",
"type": "n8n-nodes-base.itemLists", "type": "n8n-nodes-base.sort",
"typeVersion": 2, "typeVersion": 1,
"position": [ "position": [
-280, -280,
580 580
@ -182,7 +181,7 @@
"main": [ "main": [
[ [
{ {
"node": "Item Lists", "node": "Sort",
"type": "main", "type": "main",
"index": 0 "index": 0
} }
@ -200,7 +199,7 @@
] ]
] ]
}, },
"When clicking \"Execute Workflow\"": { "When clicking \"Test Workflow\"": {
"main": [ "main": [
[ [
{ {
@ -216,7 +215,7 @@
] ]
] ]
}, },
"Item Lists": { "Sort": {
"main": [ "main": [
[ [
{ {

View file

@ -0,0 +1,153 @@
{
"name": "Filter test",
"nodes": [
{
"parameters": {},
"id": "f332a7d1-31b4-4e78-b31e-9e8db945bf3f",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-60,
480
]
},
{
"parameters": {
"jsCode": "return [\n {\n \"label\": \"Apple\",\n tags: [],\n meta: {foo: 'bar'}\n },\n {\n \"label\": \"Banana\",\n tags: ['exotic'],\n meta: {}\n },\n {\n \"label\": \"Pear\",\n tags: ['other'],\n meta: {}\n },\n {\n \"label\": \"Orange\",\n meta: {}\n }\n]"
},
"id": "60697c7f-3948-4790-97ba-8aba03d02ac2",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
160,
480
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": ""
},
"conditions": [
{
"leftValue": "={{ $json.tags }}",
"rightValue": "exotic",
"operator": {
"type": "array",
"operation": "contains",
"rightType": "any"
}
},
{
"leftValue": "={{ $json.meta }}",
"rightValue": "",
"operator": {
"type": "object",
"operation": "notEmpty",
"singleValue": true
}
},
{
"leftValue": "={{ $json.label }}",
"rightValue": "Pea",
"operator": {
"type": "string",
"operation": "startsWith",
"rightType": "string"
}
}
],
"combinator": "or"
},
"options": {}
},
"id": "7531191b-5ac3-45dc-8afb-27ae83d8f33a",
"name": "If",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
380,
480
]
},
{
"parameters": {},
"id": "d8c614ea-0bbf-4b12-ad7d-c9ebe09ce583",
"name": "Then",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
600,
400
]
},
{
"parameters": {},
"id": "69364770-60d2-4ef4-9f29-9570718a9a10",
"name": "Else",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
600,
580
]
}
],
"pinData": {},
"connections": {
"When clicking \"Test Workflow\"": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"If": {
"main": [
[
{
"node": "Then",
"type": "main",
"index": 0
}
],
[
{
"node": "Else",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "a6249f48-d88f-4b80-9ed9-79555e522d48",
"id": "BWUTRs5RHxVgQ4uT",
"meta": {
"instanceId": "78577815012af39cf16dad7a787b0898c42fb7514b8a7f99b2136862c2af502c"
},
"tags": []
}

View file

@ -0,0 +1,78 @@
{
"name": "My workflow 8",
"nodes": [
{
"parameters": {
"path": "d1cba915-ca18-4425-bcfb-133205fc815a",
"formTitle": "test",
"formFields": {
"values": [
{
"fieldLabel": "test"
}
]
},
"options": {}
},
"id": "9e685367-fb94-4376-a9a4-7f311d9f7e2d",
"name": "n8n Form Trigger",
"type": "n8n-nodes-base.formTrigger",
"typeVersion": 2,
"position": [
620,
580
],
"webhookId": "d1cba915-ca18-4425-bcfb-133205fc815a"
},
{
"parameters": {},
"id": "0f4dfe66-51c0-4378-9eab-680f8140a572",
"name": "Switch",
"type": "n8n-nodes-base.switch",
"typeVersion": 2,
"position": [
800,
580
]
}
],
"pinData": {
"n8n Form Trigger": [
{
"json": {
"name": "First item",
"code": 1
}
},
{
"json": {
"name": "Second item",
"code": 2
}
}
]
},
"connections": {
"n8n Form Trigger": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "d6c14bc8-a69f-47bb-b5ba-fe6e9db0a3a4",
"id": "UQSimcMQJGbTeTLG",
"meta": {
"instanceId": "a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0"
},
"tags": []
}

View file

@ -0,0 +1,49 @@
{
"name": "Node versions",
"nodes": [
{
"parameters": {},
"id": "aadaed66-84ed-4cf8-bf21-082e9a65db76",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
1540,
780
]
},
{
"parameters": {},
"id": "93d73a85-82f0-4380-a032-713d5dc82b32",
"name": "Function",
"type": "n8n-nodes-base.function",
"typeVersion": 1,
"position": [
2040,
780
]
},
{
"id": "50f322d9-c622-4dd0-8d38-e851502739dd",
"name": "Edit Fields (old)",
"type": "n8n-nodes-base.set",
"typeVersion": 2,
"position": [
1880,
780
]
},
{
"id": "93aaadac-55fe-4618-b1eb-f63e61d1446a",
"name": "Edit Fields (latest)",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1720,
780
]
}
],
"pinData": {},
"connections": {}
}

View file

@ -47,7 +47,7 @@
{ {
"parameters": {}, "parameters": {},
"id": "58512a93-dabf-4584-817f-27c608c1bdd5", "id": "58512a93-dabf-4584-817f-27c608c1bdd5",
"name": "When clicking \"Execute Workflow\"", "name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger", "type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
@ -69,7 +69,7 @@
] ]
] ]
}, },
"When clicking \"Execute Workflow\"": { "When clicking \"Test Workflow\"": {
"main": [ "main": [
[ [
{ {

View file

@ -47,7 +47,7 @@
{ {
"parameters": {}, "parameters": {},
"id": "3dc7cf26-ff25-4437-b9fd-0e8b127ebec9", "id": "3dc7cf26-ff25-4437-b9fd-0e8b127ebec9",
"name": "When clicking \"Execute Workflow\"", "name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger", "type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
@ -552,7 +552,7 @@
] ]
] ]
}, },
"When clicking \"Execute Workflow\"": { "When clicking \"Test Workflow\"": {
"main": [ "main": [
[ [
{ {

View file

@ -0,0 +1,151 @@
{
"name": "PinData Test",
"nodes": [
{
"parameters": {},
"id": "0a60e507-7f34-41c0-a0f9-697d852033b6",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
780,
320
]
},
{
"parameters": {
"path": "b0d79ddb-df2d-49b1-8555-9fa2b482608f",
"responseMode": "lastNode",
"options": {}
},
"id": "66425ce3-450d-4aa6-a53b-a701ab89c2de",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1.1,
"position": [
780,
540
],
"webhookId": "b0d79ddb-df2d-49b1-8555-9fa2b482608f"
},
{
"parameters": {
"fields": {
"values": [
{
"name": "nodeData",
"stringValue": "init"
}
]
},
"include": "none",
"options": {}
},
"id": "3211b3c5-49e9-4694-8f86-7a5783bc653a",
"name": "Init Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1000,
320
]
},
{
"parameters": {
"fields": {
"values": [
{
"name": "nodeData",
"stringValue": "pin"
}
]
},
"options": {}
},
"id": "97b31120-4720-4632-9d35-356f345119f7",
"name": "Pin Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
1240,
320
]
},
{
"parameters": {},
"id": "1ee7be4f-7006-43bf-bb0c-29db3058a399",
"name": "End",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
1460,
320
]
}
],
"pinData": {
"Pin Data": [
{
"json": {
"nodeData": "pin-overwritten"
}
}
]
},
"connections": {
"When clicking \"Test Workflow\"": {
"main": [
[
{
"node": "Init Data",
"type": "main",
"index": 0
}
]
]
},
"Webhook": {
"main": [
[
{
"node": "Init Data",
"type": "main",
"index": 0
}
]
]
},
"Init Data": {
"main": [
[
{
"node": "Pin Data",
"type": "main",
"index": 0
}
]
]
},
"Pin Data": {
"main": [
[
{
"node": "End",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "ded8577a-3ed2-4611-842c-a7922ec58b98",
"id": "weofVLZo0ssmPDrV",
"meta": {
"instanceId": "021d3c82ba2d3bc090cbf4fc81c9312668bcc34297e022bb3438c5c88a43a5ff"
},
"tags": []
}

View file

@ -0,0 +1,53 @@
{
"meta": {
"instanceId": "2d1cf27f75b18bb9e146336f791c37884f4fc7ddb97c2def27c0444d106778bf"
},
"nodes": [
{
"parameters": {},
"id": "8108d313-8b03-4aa4-963d-cd1c0fe8f85c",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
420,
220
]
},
{
"parameters": {
"fields": {
"values": [
{
"name": "body",
"stringValue": "<?xml version=\"1.0\" encoding=\"UTF-8\"?> <library> <book> <title>Introduction to XML</title> <author>John Doe</author> <publication_year>2020</publication_year> <isbn>1234567890</isbn> </book> <book> <title>Data Science Basics</title> <author>Jane Smith</author> <publication_year>2019</publication_year> <isbn>0987654321</isbn> </book> <book> <title>Programming in Python</title> <author>Bob Johnson</author> <publication_year>2021</publication_year> <isbn>5432109876</isbn> </book> </library>"
}
]
},
"options": {}
},
"id": "45888152-7c5f-4d88-9039-660c594da084",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
640,
220
]
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,7 @@
{ {
"parameters": {}, "parameters": {},
"id": "bcb6abdf-d34b-4ea7-a8ed-58155b708c43", "id": "bcb6abdf-d34b-4ea7-a8ed-58155b708c43",
"name": "When clicking \"Execute Workflow\"", "name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger", "type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1, "typeVersion": 1,
"position": [ "position": [
@ -90,7 +90,7 @@
} }
], ],
"connections": { "connections": {
"When clicking \"Execute Workflow\"": { "When clicking \"Test Workflow\"": {
"main": [ "main": [
[ [
{ {

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,19 @@
{
"id": 60,
"name": "test1 test1",
"workflow": {
"nodes": [
{
"name": "Start",
"type": "n8n-nodes-base.start",
"position": [
250,
300
],
"parameters": {},
"typeVersion": 1
}
],
"connections": {}
}
}

View file

@ -0,0 +1,150 @@
{
"workflow": {
"id": 60,
"name": "test1 test1",
"views": 120000000,
"recentViews": 0,
"totalViews": 120000000,
"createdAt": "2019-08-30T16:39:31.362Z",
"description": "here is a description. here is a description. here is a description. \n\n![Screenshot from 20190806 091433.png](fileId:88)",
"workflow": {
"nodes": [
{
"name": "Start",
"type": "n8n-nodes-base.start",
"position": [
250,
300
],
"parameters": {},
"typeVersion": 1
}
],
"connections": {}
},
"lastUpdatedBy": null,
"workflowInfo": {
"nodeCount": 1,
"nodeTypes": {
"n8n-nodes-base.start": {
"count": 1
}
}
},
"user": {
"username": "admin"
},
"nodes": [
{
"id": 11,
"icon": "file:amqp.png",
"name": "n8n-nodes-base.amqpTrigger",
"defaults": {
"name": "AMQP Trigger"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAB7UlEQVRoge2W4W3CMBCFj26QjkBHSEdIR4AR6Ah0BBgBRqAjhBFgBBghHaEVlV29PN0lDr+o9D7JEjhn+975bJ8JIYQQQgghhBBCCCGEEA9CY2bf0NaBW2uyu7UN2XSOzTyY60J2BzNbObbsH7eTmS2mhHJHE1wmCD7A93ngEAquHaHc2omCcysSXQW74g32BHfwfTEiuCoQm9vuDsEndPYpELxKjjBj0foCEXX6XdM3by3c7aOZPZvZzMzeaBzbIh9pzIuZXaG/RqNIMAq7Ur8XCHQ2kx3LC56DMQ39X4LI23zbAd88ruRHD09wTVF5p+/eBZI5g7O8w5FgXOvsZAI7PxRwS4HGIPbm8wRjBL/Sgp/QNyQYHWySmOxgJBgFeGnPfZHgDVyufET+YMEVCdo7gziCTBbGmRKlGQpCMXOnj+1L6B0JFsxndO3cjjZyjo6OnZeqGb5gqhTQS3qKeK1SwbesfB3IrF/awqu+g8Dgs5SLE37SciHiPUv8rLVp7k2wdl63tDDqgTs8lqpINWGXbSTKe9rlJgXME7C9I6V7oGAWsEzv2gzeN2TstkbCZyIJWBYKWUwtF4foKGU9TpRGdZDSdVDpDNXSVVBLt5TeucS9K6X/E3USX3rshBBCCCGEEEIIIYQQ4tExsx8PuuPnwhCIbgAAAABJRU5ErkJggg=="
},
"categories": [
{
"id": 5,
"name": "Development"
},
{
"id": 6,
"name": "Communication"
}
],
"displayName": "AMQP Trigger",
"typeVersion": 1
},
{
"id": 18,
"icon": "file:autopilot.svg",
"name": "n8n-nodes-base.autopilot",
"defaults": {
"name": "Autopilot"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjM4IDI2IDM1IDM1Ij48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSIjMThkNGIyIiBzdHJva2Utd2lkdGg9IjMiIGZpbGw9IiMxOGQ0YjIiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNDUuNCA0Mi42aDE5LjlsMy40LTQuOEg0MmwzLjQgNC44em0zLjEgOC4zaDEzLjFsMy40LTQuOEg0NS40bDMuMSA0Ljh6bTU0LS43Ii8+PC9zdmc+"
},
"categories": [
{
"id": 1,
"name": "Marketing"
}
],
"displayName": "Autopilot",
"typeVersion": 1
},
{
"id": 20,
"icon": "file:lambda.svg",
"name": "n8n-nodes-base.awsLambda",
"defaults": {
"name": "AWS Lambda"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI1MDAiIHZpZXdCb3g9Ii0zLjAyMyAtMC4yMiA0MjAuOTIzIDQzMy41NCIgd2lkdGg9IjI0NDMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIwOC40NSAyMjcuODljLTEuNTkgMi4yNi0yLjkzIDQuMTItNC4yMiA2cS0zMC44NiA0NS40Mi02MS43IDkwLjgzLTI4LjY5IDQyLjI0LTU3LjQ0IDg0LjQzYTMuODggMy44OCAwIDAxLTIuNzMgMS41OXEtNDAuNTktLjM1LTgxLjE2LS44OGMtLjMgMC0uNjEtLjA5LTEuMi0uMThhMTQuNDQgMTQuNDQgMCAwMS43Ni0xLjY1cTI4LjMxLTQzLjg5IDU2LjYyLTg3Ljc2IDI1LjExLTM4Ljg4IDUwLjI1LTc3Ljc0IDI3Ljg2LTQzLjE4IDU1LjY5LTg2LjQyYzIuNzQtNC4yNSA1LjU5LTguNDIgOC4xOS0xMi43NWE1LjI2IDUuMjYgMCAwMC41Ni0zLjgzYy01LTE1Ljk0LTEwLjEtMzEuODQtMTUuMTktNDcuNzQtMi4xOC02LjgxLTQuNDYtMTMuNTgtNi41LTIwLjQzLS42Ni0yLjItMS43NS0yLjg3LTQtMi44Ni0xNyAuMDctMzMuOS4wNS01MC44NS4wNS0zLjIyIDAtMy4yMyAwLTMuMjMtMy4xOCAwLTIwLjg0IDAtNDEuNjgtLjA2LTYyLjUyIDAtMi4zMi43Ni0yLjg0IDIuOTQtMi44NHE1MS4xOS4wOSAxMDIuNCAwYTMuMjkgMy4yOSAwIDAxMy42IDIuNDNxMjcgNjcuOTEgNTQgMTM1Ljc3IDMxLjUgNzkuMTQgNjMgMTU4LjNjNi41MiAxNi4zOCAxMy4wOSAzMi43NSAxOS41NCA0OS4xNy43NyAyIDEuNTcgMi4zOCAzLjU5IDEuNzYgMTcuODktNS41MyAzNS44Mi0xMC45MSA1My43LTE2LjQ1IDIuMjUtLjcgMy4wNy0uMjMgMy43NyAyIDYuMSAxOS4xNyAxMi4zMiAzOC4zIDE4LjUgNTcuNDUuMjEuNjYuMzcgMS4zMy42MiAyLjI1LTEuMjguNDctMi40OCAxLTMuNzEgMS4zNHEtNjEgMTkuMzMtMTIxLjkzIDM4LjY4Yy0xLjk0LjYxLTIuNTItLjA1LTMuMTctMS42OHEtMTguNjEtNDcuMTYtMzcuMzEtOTQuMjgtMTguMjktNDYuMTQtMzYuNi05Mi4yOGMtMS44My00LjYyLTMuNjMtOS4yNi01LjQ2LTEzLjg4LS4yOS0uNzktLjY5LTEuNDgtMS4yNy0yLjd6IiBmaWxsPSIjZmE3ZTE0Ii8+PC9zdmc+"
},
"categories": [
{
"id": 5,
"name": "Development"
}
],
"displayName": "AWS Lambda",
"typeVersion": 1
},
{
"id": 40,
"icon": "file:clearbit.svg",
"name": "n8n-nodes-base.clearbit",
"defaults": {
"name": "Clearbit"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI3MiIgaGVpZ2h0PSI3MiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iNTAlIiB4Mj0iMTAwJSIgeTE9IjAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI0RFRjJGRSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI0RCRjFGRSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJiIiB4MT0iMCUiIHgyPSI1MCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM1N0JDRkQiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM1MUI1RkQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjM3LjUlIiB4Mj0iNjIuNSUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiMxQ0E3RkQiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMxNDhDRkMiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGZpbGw9InVybCgjYSkiIGQ9Ik03MiAzNnYxNi43N2wtLjAwNC44NjhjLS4wNiA2LjAzNS0uNzUgOC4zNTMtMiAxMC42ODhhMTMuNjMgMTMuNjMgMCAwMS01LjY3IDUuNjdsLS4zMjYuMTcxQzYxLjY1OCA3MS4zNjQgNTkuMTYgNzIgNTIuNzcgNzJIMzZWMzZoMzZ6Ii8+PHBhdGggZmlsbD0idXJsKCNiKSIgZD0iTTY0LjMyNiAyLjAwM2ExMy42MyAxMy42MyAwIDAxNS42NyA1LjY3bC4xNzEuMzI3QzcxLjM2NCAxMC4zNDIgNzIgMTIuODQgNzIgMTkuMjNWMzZIMzZWMGgxNi43N2M2LjY4NyAwIDkuMTEyLjY5NiAxMS41NTYgMi4wMDN6Ii8+PHBhdGggZmlsbD0idXJsKCNjKSIgZD0iTTM2IDB2NzJIMTkuMjNsLS44NjgtLjAwNGMtNi4wMzUtLjA2LTguMzUzLS43NS0xMC42ODgtMmExMy42MyAxMy42MyAwIDAxLTUuNjctNS42N0wxLjgzMiA2NEMuNjM2IDYxLjY1OCAwIDU5LjE2IDAgNTIuNzdWMTkuMjNjMC02LjY4Ny42OTYtOS4xMTIgMi4wMDMtMTEuNTU2YTEzLjYzIDEzLjYzIDAgMDE1LjY3LTUuNjdMOCAxLjgzMkMxMC4zNDIuNjM2IDEyLjg0IDAgMTkuMjMgMEgzNnoiLz48L2c+PC9zdmc+"
},
"categories": [
{
"id": 2,
"name": "Sales"
}
],
"displayName": "Clearbit",
"typeVersion": 1
},
{
"id": 51,
"icon": "file:convertKit.svg",
"name": "n8n-nodes-base.convertKitTrigger",
"defaults": {
"name": "ConvertKit Trigger"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTcyIiBoZWlnaHQ9IjE2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNODIuNzIgMTI2LjMxNmMyOS43NyAwIDUyLjc4LTIyLjYyMiA1Mi43OC01MC41MjYgMC0yNi4xNDMtMjEuNjE3LTQyLjEwNi0zNS45MzUtNDIuMTA2LTE5Ljk0NSAwLTM1LjkzIDE0LjA4NC0zOC4xOTggMzQuOTg4LS40MTggMy44NTYtMy40NzYgNy4wOS03LjM1NSA3LjA2MS02LjQyMy0uMDQ2LTE1Ljc0Ni0uMS0yMS42NTgtLjA4LTIuNTU1LjAwOC00LjY2OS0yLjA2NS00LjU0My00LjYxOC44OS0xOC4xMjMgNi45MTQtMzUuMDcgMTguNDAyLTQ4LjA4N0M1OC45NzYgOC40ODggNzcuNTYxIDAgOTkuNTY1IDBjMzYuOTY5IDAgNzEuODY5IDMzLjc4NiA3MS44NjkgNzUuNzkgMCA0Ni41MDgtMzguMzEyIDg0LjIxLTg3LjkyNyA4NC4yMS0zNS4zODQgMC03MS4wMjEtMjMuMjU4LTgzLjQ2NC01NS43NzVhLjcwMi43MDIgMCAwMS0uMDMtLjM3N2MuMTY1LS45NjIuNDk0LTEuODQxLjgxOC0yLjcwNy40NzEtMS4yNTguOTMxLTIuNDg4Ljg2NC0zLjkwNmwtLjIxNS00LjUyOWE1LjUyMyA1LjUyMyAwIDAxMy4xOC01LjI2M2wxLjc5OC0uODQyYTYuOTgyIDYuOTgyIDAgMDAzLjkxMi01LjA3NSA2Ljk5MyA2Ljk5MyAwIDAxNi44ODctNS43MzZjNS4yODIgMCA5Ljg3NSAzLjUxNSAxMS41OSA4LjUxMiA4LjMwNyAyNC4yMTIgMjEuNTExIDQyLjAxNCA1My44NzMgNDIuMDE0eiIgZmlsbD0iI0ZCNjk3MCIvPjwvc3ZnPg=="
},
"categories": [
{
"id": 1,
"name": "Marketing"
},
{
"id": 2,
"name": "Sales"
}
],
"displayName": "ConvertKit Trigger",
"typeVersion": 1
}
],
"categories": [],
"image": []
}
}

View file

@ -0,0 +1,52 @@
{
"meta": {
"instanceId": "123"
},
"nodes": [
{
"parameters": {
"resource": "credential",
"name": "123",
"credentialTypeName": "123"
},
"id": "a01f79f6-e8c3-44c5-be5e-4bc482e23172",
"name": "n8n",
"type": "n8n-nodes-base.n8n",
"typeVersion": 1,
"position": [
540,
240
],
"credentials": {
"n8nApi": {
"id": "10",
"name": "n8n account"
}
}
},
{
"parameters": {},
"id": "acdd1bdc-c642-4ea6-ad67-f4201b640cfa",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
300,
240
]
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"main": [
[
{
"node": "n8n",
"type": "main",
"index": 0
}
]
]
}
}
}

View file

@ -0,0 +1,90 @@
{
"meta": {
"instanceId": "15bbf37b6a515ccc2f534cabcd8bd171ca33583ff7744b1e9420e5ce68e615bb"
},
"nodes": [
{
"parameters": {},
"id": "40720511-19b6-4421-bdb0-3fb6efef4bc5",
"name": "When clicking \"Test Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
280,
320
]
},
{
"parameters": {},
"id": "acdd1bdc-c642-4ea6-ad67-f4201b640cfa",
"name": "Unknown node 1",
"type": "n8n-nodes-base.thisNodeDoesntExist",
"typeVersion": 1,
"position": [
400,
500
]
},
{
"parameters": {},
"id": "acdd1bdc-c642-4ea6-ad67-f4201b640ffa",
"name": "Unknown node 2",
"type": "n8n-nodes-base.thisNodeDoesntExistEither",
"typeVersion": 1,
"position": [
600,
500
]
},
{
"parameters": {
"options": {}
},
"id": "fbe5163b-7474-4741-980a-e4956789be0a",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
500,
320
]
},
{
"parameters": {
"options": {}
},
"id": "163313b9-64ff-4ffc-b00f-09b267d8132c",
"name": "Edit Fields1",
"type": "n8n-nodes-base.set",
"typeVersion": 3.2,
"position": [
720,
320
]
}
],
"connections": {
"When clicking \"Test Workflow\"": {
"main": [
[
{
"node": "Edit Fields",
"type": "main",
"index": 0
}
]
]
},
"Edit Fields": {
"main": [
[
{
"node": "Edit Fields1",
"type": "main",
"index": 0
}
]
]
}
}
}

21
cypress/pages/demo.ts Normal file
View file

@ -0,0 +1,21 @@
/**
* Actions
*/
export function vistDemoPage(theme?: 'dark' | 'light') {
const query = theme ? `?theme=${theme}` : '';
cy.visit('/workflows/demo' + query);
cy.waitForLoad();
cy.window().then((win) => {
// @ts-ignore
win.preventNodeViewBeforeUnload = true;
});
}
export function importWorkflow(workflow: object) {
const OPEN_WORKFLOW = {command: 'openWorkflow', workflow};
cy.window().then($window => {
const message = JSON.stringify(OPEN_WORKFLOW);
$window.postMessage(message, '*')
});
}

View file

@ -20,6 +20,7 @@ export class NodeCreator extends BasePage {
communityNodeTooltip: () => cy.getByTestId('node-item-community-tooltip'), communityNodeTooltip: () => cy.getByTestId('node-item-community-tooltip'),
noResults: () => cy.getByTestId('node-creator-no-results'), noResults: () => cy.getByTestId('node-creator-no-results'),
nodeItemName: () => cy.getByTestId('node-creator-item-name'), nodeItemName: () => cy.getByTestId('node-creator-item-name'),
nodeItemDescription: () => cy.getByTestId('node-creator-item-description'),
activeSubcategory: () => cy.getByTestId('nodes-list-header'), activeSubcategory: () => cy.getByTestId('nodes-list-header'),
expandedCategories: () => expandedCategories: () =>
this.getters.creatorItem().find('>div').filter('.active').invoke('text'), this.getters.creatorItem().find('>div').filter('.active').invoke('text'),

View file

@ -10,3 +10,6 @@ export * from './ndv';
export * from './bannerStack'; export * from './bannerStack';
export * from './workflow-executions-tab'; export * from './workflow-executions-tab';
export * from './signin'; export * from './signin';
export * from './workflow-history';
export * from './workerView';
export * from './settings-public-api';

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